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; 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%;
} }

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

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

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

@ -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 { val bookFolderNames = ArrayList<String>()
it.getFolderName() val originNames = ArrayList<String>()
appDb.bookDao.all.forEach {
bookFolderNames.add(it.getFolderName())
if (it.isEpub()) originNames.add(it.originName)
} }
val file = downloadDir.getFile(cacheFolderName) downloadDir.getFile(cacheFolderName)
file.listFiles()?.forEach { bookFile -> .listFiles()?.forEach { bookFile ->
if (!bookFolderNames.contains(bookFile.name)) { if (!bookFolderNames.contains(bookFile.name)) {
FileUtils.delete(bookFile.absolutePath) 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("文件不存在")

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

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

@ -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()
}
}
/** /**
* 上传文件 * 上传文件
*/ */

@ -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 cover = BitmapFactory.decodeStream(input)
val out = val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
cover.compress(Bitmap.CompressFormat.JPEG, 90, out) out.flush()
out.flush() out.close()
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
} }

@ -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) val list = ArrayList(LinkedHashSet(chapters))
lh.forEachIndexed { index, bookChapter -> bookChapter.index = index } list.forEachIndexed { index, bookChapter -> bookChapter.index = index }
return ArrayList(lh) 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.isAbsUrl() -> AnalyzeUrl(str, source = source).getInputStream()
str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT) 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)
} }

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

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

@ -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()
} }
} }

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

@ -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("自动同步阅读进度成功")
} }
} }

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

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

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

@ -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 height: Int? = null,
): Bitmap { block: (() -> Unit)? = null
): Bitmap? {
//src为空白时 可能被净化替换掉了 或者规则失效 //src为空白时 可能被净化替换掉了 或者规则失效
if (book.getUseReplaceRule() && src.isBlank()) { if (book.getUseReplaceRule() && src.isBlank()) {
book.setUseReplaceRule(false) book.setUseReplaceRule(false)
appCtx.toastOnUi(R.string.error_image_url_empty) 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
}
} }

@ -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 -> webdav.downloadInputStream().let { inputStream ->
LocalBook.saveBookFile(bytes, remoteBook.filename) 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.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) {

@ -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) alert(R.string.draw) {
viewModel.delete(rule) 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) {

@ -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) {

@ -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 设备上下文

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

@ -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 splitties.init.appCtx
import java.io.ByteArrayOutputStream 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"])

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

Loading…
Cancel
Save