Merge remote-tracking branch 'upstream/master'

pull/2250/head
Seidko 2 years ago
commit 1f90bf8d8c
  1. BIN
      app/cronetlib/cronet_api.jar
  2. BIN
      app/cronetlib/cronet_impl_common_java.jar
  3. 2
      app/src/main/assets/cronet.json
  4. 2
      app/src/main/assets/epub/main.css
  5. 5
      app/src/main/assets/updateLog.md
  6. 1
      app/src/main/assets/web/bookSource/css/app.1bd41080.css
  7. 1
      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. 150
      app/src/main/assets/web/bookSource/index.css
  11. 440
      app/src/main/assets/web/bookSource/index.html
  12. 516
      app/src/main/assets/web/bookSource/index.js
  13. 2
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js
  14. 1
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map
  15. 2
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js
  16. 1
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map
  17. 14
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js
  18. 1
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map
  19. 2
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  20. 18
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  21. 27
      app/src/main/java/io/legado/app/help/BookHelp.kt
  22. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  23. 8
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  24. 48
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  25. 18
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  26. 19
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  27. 3
      app/src/main/java/io/legado/app/model/localBook/TextFile.kt
  28. 2
      app/src/main/java/io/legado/app/model/localBook/UmdFile.kt
  29. 10
      app/src/main/java/io/legado/app/ui/book/local/rule/TxtTocRuleActivity.kt
  30. 6
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  31. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  32. 37
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  33. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  34. 2
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  35. 51
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt
  36. 4
      app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt
  37. 11
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  38. 22
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  39. 8
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  40. 18
      app/src/main/java/io/legado/app/utils/BitmapUtils.kt
  41. 2
      app/src/main/java/io/legado/app/utils/HtmlFormatter.kt
  42. 27
      app/src/main/java/io/legado/app/web/HttpServer.kt
  43. 2
      gradle.properties

Binary file not shown.

@ -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"}

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

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

@ -0,0 +1 @@
@font-face{font-family:Cascadia;src:url(../fonts/Cascadia-Code-Regular-2.97c09831.ttf)}.editor{display:flex;align-items:stretch;height:100vh}.left{width:calc(50% - 55px);min-width:500px;display:inline-block}.main{width:100%;height:calc(100vh - 64px);overflow-y:auto}textarea{min-height:20px;flex:1;outline:none;border-radius:4px;border:1px solid #ccc;font-size:14px;font-family:Cascadia,Microsoft YaHei UI,Arial,serif;line-height:20px;padding:10px 10px 10px}.fixed[data-v-ab8a8a34]{position:fixed;top:100px;left:50%;transform:translateX(-50%)}.half-circle-spinner [data-v-ab8a8a34],.half-circle-spinner[data-v-ab8a8a34]{box-sizing:border-box}.half-circle-spinner[data-v-ab8a8a34]{width:60px;height:60px;border-radius:100%;position:relative}.half-circle-spinner .circle[data-v-ab8a8a34]{content:"";position:absolute;width:100%;height:100%;border-radius:100%;border:6px solid transparent}.half-circle-spinner .circle.circle-1[data-v-ab8a8a34]{border-top-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite;animation:half-circle-spinner-animation-ab8a8a34 1s infinite}.half-circle-spinner .circle.circle-2[data-v-ab8a8a34]{border-bottom-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate;animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate}@-webkit-keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.warm[data-v-27eae50d]{position:fixed;top:100px;left:50%;transform:translateX(-50%);color:#eee;background-color:#ff1d5e;padding:20px;border-radius:8px;font-size:18px}button[data-v-27eae50d]{float:right;width:70px;height:35px;outline:none;border:unset;border-radius:8px;font-size:14px;transition:background-color .3s ease}.success[data-v-bef92f74],button[data-v-27eae50d]:hover{color:#333;background-color:#7fffd4}.success[data-v-bef92f74]{z-index:3;position:fixed;top:100px;left:50%;transform:translateX(-50%);padding:10px;border-radius:8px}p[data-v-bef92f74]{margin:0}.menu{display:flex;flex-direction:column;justify-content:center}button[data-v-8302f478]{width:100px;height:50px;margin:5px;outline:none;padding:10px;font-size:16px;border-radius:4px;box-sizing:border-box;border:1px solid #333;background-color:transparent;transition:all .3s ease;overflow:hidden}button span[data-v-8302f478]{display:inline-block;transform:translateY(20px);opacity:0;font-size:12px;transition:all .3s ease}button[data-v-8302f478]:hover{color:#fff;background-color:gray}button:hover span[data-v-8302f478]{transform:translateY(0);opacity:1}textarea[data-v-4301c68c]{font-size:14px}input[data-v-f11ea0d0]{outline:none;height:25px;font-size:16px}textarea[data-v-f11ea0d0]{font-size:14px}.search[data-v-260cb454]{border:1px solid #ddd;border-radius:4px}input[data-v-260cb454]{text-align:center;font-size:16px;height:25px;outline:none}.tool[data-v-260cb454]{display:flex;justify-content:space-around;padding:4px 0}.tool button[data-v-260cb454]{border-radius:4px;padding:5px;outline:none;border:none;flex:1;margin:0 2px}.tool button[data-v-260cb454]:hover{background-color:#ddd}input[type=checkbox][data-v-260cb454]{transform:scale(2);margin:0 15px 0}.book_item[data-v-260cb454]{overflow:hidden;display:flex;align-items:center;margin-top:10px;padding:10px;cursor:pointer;background-color:#eee}.book_active[data-v-260cb454]{background-color:#32cd32}.book_index[data-v-260cb454]{width:auto;height:30px;text-align:center;line-height:30px;border-radius:8px;padding:0 10px;background-color:#ffb6c1}.book_info[data-v-260cb454]{display:flex;justify-content:space-between}.book_list[data-v-260cb454]{height:calc(100vh - 125px);overflow-y:auto}a[data-v-c4c82b2c]{display:block;height:25px;transition:margin-left .3s ease}a[data-v-c4c82b2c]:hover{color:red;margin-left:10px}code[data-v-c4c82b2c]{display:inline-block;font-size:16px;font-family:Cascadia Mono,JetBrains Mono,Microsoft YaHei UI,serif;background-color:#f3f4f4;border-radius:4px}.author[data-v-c4c82b2c]{position:fixed;right:20px;bottom:20px}.author a[data-v-c4c82b2c]{padding:5px 10px;border-radius:4px}.author a[data-v-c4c82b2c]:hover{box-shadow:0 0 3px 3px #ccc;background-color:#ddd}.out{min-width:620px}.out,.out .box{flex:1;display:flex;flex-flow:column}a{text-decoration:none;color:#333}body,ul{margin:0;padding:0}ul{width:100%;height:40px;display:flex}ul li{display:block;text-align:center;line-height:40px;width:16%;list-style:none;cursor:pointer}ul li:hover{color:red}.active{color:#fff!important;background-color:hsla(0,0%,50%,.459);border-bottom:unset!important}ul li[data-v-de4729c0]{border-bottom:2px solid #333}a[data-v-de4729c0]{font-size:18px;color:blue}span[data-v-3974490a]{display:flex;justify-content:space-around;width:70px;white-space:nowrap}span i[data-v-3974490a]{font-style:normal}.edit_input[data-v-3974490a]{display:flex;justify-content:space-between;align-items:center;margin:10px 5px}

@ -0,0 +1 @@
@font-face{font-family:Cascadia;src:url(../fonts/Cascadia-Code-Regular-2.97c09831.ttf)}.editor{display:flex;align-items:stretch;height:100vh}.left{width:calc(50% - 55px);min-width:500px;display:inline-block}.main{width:100%;height:calc(100vh - 64px);overflow-y:auto}textarea{min-height:20px;flex:1;outline:none;border-radius:4px;border:1px solid #ccc;font-size:14px;font-family:Cascadia,Microsoft YaHei UI,Arial,serif;line-height:20px;padding:10px 10px 10px}.fixed[data-v-ab8a8a34]{position:fixed;top:100px;left:50%;transform:translateX(-50%)}.half-circle-spinner [data-v-ab8a8a34],.half-circle-spinner[data-v-ab8a8a34]{box-sizing:border-box}.half-circle-spinner[data-v-ab8a8a34]{width:60px;height:60px;border-radius:100%;position:relative}.half-circle-spinner .circle[data-v-ab8a8a34]{content:"";position:absolute;width:100%;height:100%;border-radius:100%;border:6px solid transparent}.half-circle-spinner .circle.circle-1[data-v-ab8a8a34]{border-top-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite;animation:half-circle-spinner-animation-ab8a8a34 1s infinite}.half-circle-spinner .circle.circle-2[data-v-ab8a8a34]{border-bottom-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate;animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate}@-webkit-keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.warm[data-v-27eae50d]{position:fixed;top:100px;left:50%;transform:translateX(-50%);color:#eee;background-color:#ff1d5e;padding:20px;border-radius:8px;font-size:18px}button[data-v-27eae50d]{float:right;width:70px;height:35px;outline:none;border:unset;border-radius:8px;font-size:14px;transition:background-color .3s ease}.success[data-v-bef92f74],button[data-v-27eae50d]:hover{color:#333;background-color:#7fffd4}.success[data-v-bef92f74]{z-index:3;position:fixed;top:100px;left:50%;transform:translateX(-50%);padding:10px;border-radius:8px}p[data-v-bef92f74]{margin:0}.menu{display:flex;flex-direction:column;justify-content:center}button[data-v-7d09e592]{width:100px;height:50px;margin:5px;outline:none;padding:10px;font-size:16px;border-radius:4px;box-sizing:border-box;border:1px solid #333;background-color:transparent;transition:all .3s ease;overflow:hidden}button span[data-v-7d09e592]{display:inline-block;transform:translateY(20px);opacity:0;font-size:12px;transition:all .3s ease}button[data-v-7d09e592]:hover{color:#fff;background-color:gray}button:hover span[data-v-7d09e592]{transform:translateY(0);opacity:1}textarea[data-v-4301c68c]{font-size:14px}input[data-v-f11ea0d0]{outline:none;height:25px;font-size:16px}textarea[data-v-f11ea0d0]{font-size:14px}.search[data-v-260cb454]{border:1px solid #ddd;border-radius:4px}input[data-v-260cb454]{text-align:center;font-size:16px;height:25px;outline:none}.tool[data-v-260cb454]{display:flex;justify-content:space-around;padding:4px 0}.tool button[data-v-260cb454]{border-radius:4px;padding:5px;outline:none;border:none;flex:1;margin:0 2px}.tool button[data-v-260cb454]:hover{background-color:#ddd}input[type=checkbox][data-v-260cb454]{transform:scale(2);margin:0 15px 0}.book_item[data-v-260cb454]{overflow:hidden;display:flex;align-items:center;margin-top:10px;padding:10px;cursor:pointer;background-color:#eee}.book_active[data-v-260cb454]{background-color:#32cd32}.book_index[data-v-260cb454]{width:auto;height:30px;text-align:center;line-height:30px;border-radius:8px;padding:0 10px;background-color:#ffb6c1}.book_info[data-v-260cb454]{display:flex;justify-content:space-between}.book_list[data-v-260cb454]{height:calc(100vh - 125px);overflow-y:auto}a[data-v-c4c82b2c]{display:block;height:25px;transition:margin-left .3s ease}a[data-v-c4c82b2c]:hover{color:red;margin-left:10px}code[data-v-c4c82b2c]{display:inline-block;font-size:16px;font-family:Cascadia Mono,JetBrains Mono,Microsoft YaHei UI,serif;background-color:#f3f4f4;border-radius:4px}.author[data-v-c4c82b2c]{position:fixed;right:20px;bottom:20px}.author a[data-v-c4c82b2c]{padding:5px 10px;border-radius:4px}.author a[data-v-c4c82b2c]:hover{box-shadow:0 0 3px 3px #ccc;background-color:#ddd}.out{min-width:620px}.out,.out .box{flex:1;display:flex;flex-flow:column}a{text-decoration:none;color:#333}body,ul{margin:0;padding:0}ul{width:100%;height:40px;display:flex}ul li{display:block;text-align:center;line-height:40px;width:16%;list-style:none;cursor:pointer}ul li:hover{color:red}.active{color:#fff!important;background-color:hsla(0,0%,50%,.459);border-bottom:unset!important}ul li[data-v-de4729c0]{border-bottom:2px solid #333}a[data-v-de4729c0]{font-size:18px;color:blue}span[data-v-3974490a]{display:flex;justify-content:space-around;width:70px;white-space:nowrap}span i[data-v-3974490a]{font-style:normal}.edit_input[data-v-3974490a]{display:flex;justify-content:space-between;align-items:center;margin:10px 5px}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -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 +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>

@ -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:
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

@ -31,6 +31,7 @@ object BookHelp {
private val downloadDir: File = appCtx.externalFiles
private const val cacheFolderName = "book_cache"
private const val cacheImageFolderName = "images"
private const val cacheEpubFolderName = "epub"
private val downloadImages = CopyOnWriteArraySet<String>()
fun clearCache() {
@ -55,15 +56,24 @@ object BookHelp {
*/
suspend fun clearInvalidCache() {
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 {
val uri = Uri.parse(book.bookUrl)
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 doc = DocumentFile.fromSingleUri(appCtx, uri)
?: throw IOException("文件不存在")

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

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

@ -23,6 +23,8 @@ import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.net.URLEncoder
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()
}
}
/**
* 上传文件
*/

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

@ -78,9 +78,12 @@ object LocalBook {
if (chapters.isEmpty()) {
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? {
@ -232,8 +235,8 @@ object LocalBook {
AppConfig.defaultBookTreeUri
?: throw NoStackTraceException("没有设置书籍保存位置!")
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")
}
return saveBookFile(bytes, fileName)
@ -254,7 +257,7 @@ object LocalBook {
}
fun saveBookFile(
bytes: ByteArray,
inputStream: InputStream,
fileName: String
): Uri {
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
@ -268,14 +271,14 @@ object LocalBook {
?: throw SecurityException("Permission Denial")
}
appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
oStream.write(bytes)
inputStream.copyTo(oStream)
}
doc.uri
} else {
val treeFile = File(treeUri.path!!)
val file = treeFile.getFile(fileName)
FileOutputStream(file).use { oStream ->
oStream.write(bytes)
inputStream.copyTo(oStream)
}
Uri.fromFile(file)
}

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

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

@ -88,7 +88,13 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
}
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) {
@ -151,7 +157,7 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
private fun delSourceDialog() {
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
okButton { viewModel.del(*adapter.selection.toTypedArray()) }
yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
noButton()
}
}

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

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

@ -7,6 +7,7 @@ import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
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)
var textPage: TextPage = TextPage()
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
@ -86,6 +91,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
super.onDraw(canvas)
canvas.clipRect(visibleRect)
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 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(
book,
textChar.charData,
(textChar.end - textChar.start).toInt(),
(lineBottom - lineTop).toInt()
)
) {
if (!drawVisibleImageOnly && isVisible) {
drawVisibleImageOnly = true
invalidate()
}
} ?: return
val rectF = if (textLine.isImage) {
RectF(textChar.start, lineTop, textChar.end, lineBottom)
} else {

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

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

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

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

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

@ -162,7 +162,10 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}
override fun onClickSelectBarMainAction() {
delSourceDialog()
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
yesButton { viewModel.delSelection(adapter.selection) }
noButton()
}
}
private fun initSelectActionView() {
@ -172,13 +175,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
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) {
dataInit = false
replaceRuleFlowJob?.cancel()
@ -329,8 +325,14 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}
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) {

@ -329,7 +329,13 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
}
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) {

@ -10,7 +10,6 @@ import android.graphics.Color
import com.google.android.renderscript.Toolkit
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
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 设备上下文

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

@ -7,9 +7,11 @@ import io.legado.app.api.ReturnData
import io.legado.app.api.controller.BookController
import io.legado.app.api.controller.BookSourceController
import io.legado.app.api.controller.RssSourceController
import io.legado.app.utils.FileUtils
import io.legado.app.utils.externalFiles
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) {
@ -91,7 +93,26 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
byteArray.size.toLong()
)
} 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-Origin", session.headers["origin"])

@ -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.
android.nonTransitiveRClass=true
#https://chromiumdash.appspot.com/releases?platform=Android
CronetVersion=105.0.5195.68
CronetVersion=105.0.5195.77

Loading…
Cancel
Save