diff --git a/app/src/main/assets/disclaimer.md b/app/src/main/assets/disclaimer.md
new file mode 100644
index 000000000..0f630a9e2
--- /dev/null
+++ b/app/src/main/assets/disclaimer.md
@@ -0,0 +1,16 @@
+# 免责声明(Disclaimer)
+
+* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
+* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。
+各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。
+任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
+第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。
+您应该对使用搜索引擎的结果自行承担风险。
+* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。
+因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。
+阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。
+* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。
+阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。
+阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。
+任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。
+阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。
\ No newline at end of file
diff --git a/app/src/main/assets/number.ttf b/app/src/main/assets/number.ttf
new file mode 100644
index 000000000..f2804dbe9
Binary files /dev/null and b/app/src/main/assets/number.ttf differ
diff --git a/app/src/main/assets/txtChapterRule.json b/app/src/main/assets/txtChapterRule.json
new file mode 100644
index 000000000..b035fccb5
--- /dev/null
+++ b/app/src/main/assets/txtChapterRule.json
@@ -0,0 +1,38 @@
+[
+ {
+ "enable": true,
+ "name": "默认正则1",
+ "rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$",
+ "serialNumber": 0
+ },
+ {
+ "enable": true,
+ "name": "默认正则2",
+ "rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$",
+ "serialNumber": 1
+ },
+ {
+ "enable": true,
+ "name": "默认正则3",
+ "rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$",
+ "serialNumber": 2
+ },
+ {
+ "enable": true,
+ "name": "默认正则4",
+ "rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$",
+ "serialNumber": 3
+ },
+ {
+ "enable": true,
+ "name": "默认正则5",
+ "rule": "^(\\s{0,4})(正文)(.{0,20})$",
+ "serialNumber": 4
+ },
+ {
+ "enable": true,
+ "name": "默认正则6",
+ "rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$",
+ "serialNumber": 5
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md
new file mode 100644
index 000000000..8038b0c77
--- /dev/null
+++ b/app/src/main/assets/updateLog.md
@@ -0,0 +1,5 @@
+## 本软件为开源软件,没有上架Google Play,请不要在任何地方购买!
+### 关注公众号[开源阅读软件]可获取书源,点击广告可支持作者!
+### 捐赠里点击红包搜索码可开启高级功能!
+## 更新日志
+
diff --git a/app/src/main/assets/web/bookshelf.css b/app/src/main/assets/web/bookshelf.css
new file mode 100644
index 000000000..f6e26caa9
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.css
@@ -0,0 +1,176 @@
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+.hide {
+ display: none;
+}
+
+.top, .showchapter, .hidebooks {
+ width: 60px;
+ height: 50px;
+ position: absolute;
+ right: 30px;
+ bottom: 30px;
+ color: black;
+ font-size: 28px;
+ background-color: #ddd;
+ opacity: 0.85;
+}
+
+.top {
+ bottom: 150px;
+}
+
+.showchapter {
+ bottom: 90px;
+ bottom: 90px;
+}
+
+.address {
+ width: 270px;
+}
+
+.nav {
+ border-bottom: solid 1px #ccc;
+}
+
+input, button {
+ width: 110px;
+ line-height: 34px;
+ background-color: #eee;
+ color: #555;
+ border: none;
+ margin: 10px 5px;
+ font-weight: 500;
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+}
+
+input {
+ padding: 0 10px;
+ cursor: text;
+}
+
+ input:hover, button:hover {
+ border-color: #aaa;
+ background-color: #efefef;
+ color: #222;
+ outline: solid 1px #ccc;
+ }
+
+.allcontent {
+ height: calc(100% - 60px);
+}
+
+.allscreen {
+ height: 100%
+}
+
+.books > div {
+ display: inline-block;
+ margin: 10px;
+ vertical-align: top;
+ border: solid 1px #ddd;
+}
+
+.read > .books {
+ width: 420px;
+ float: left;
+ height: 100%;
+ overflow: auto;
+ border-right: solid 1px #ccc;
+}
+
+ .read > .books > div {
+ margin-right: 0;
+ border-right: none;
+ }
+
+
+.more {
+ overflow-y: auto;
+ height: 100%;
+ display: none;
+}
+
+.read .more {
+ display: block;
+}
+
+.books > div > img {
+ width: 120px;
+ height: 180px;
+ float: left;
+ margin-right: 10px;
+ cursor: pointer;
+}
+
+.info {
+ padding: 10px 20px 0 20px;
+ width: 600px;
+ margin: 0 auto;
+}
+
+ .info > img {
+ width: 600px;
+ height: 900px;
+ }
+
+ .info p {
+ line-height: 1.5;
+ text-align: justify;
+ margin: 0;
+ }
+
+.books tr:nth-child(n+2) td {
+ border-top: solid 1px #999;
+}
+
+.books td:nth-child(1) {
+ vertical-align: top;
+ width: 50px;
+}
+
+.books td:nth-child(2) {
+ vertical-align: top;
+ width: 200px;
+}
+
+.clear {
+ clear: both;
+}
+
+.chapter {
+ margin: 10px;
+ max-height: 500px;
+ overflow-y: auto;
+ border-top: solid 1px #333;
+ border-bottom: solid 1px #333;
+}
+
+ .chapter button {
+ width: 230px;
+ text-align: left;
+ text-indent: 14px;
+ margin: 10px 4px;
+ }
+
+
+.content {
+ padding: 20px;
+ text-align: justify;
+ min-height: 1000px;
+ padding-bottom: 200px;
+}
+
+ .content h2 {
+ font-family: "Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu;
+ font-weight: 500;
+ text-align: center;
+ line-height: 100px;
+ font-size: 40px;
+ margin: 0;
+ }
diff --git a/app/src/main/assets/web/bookshelf.html b/app/src/main/assets/web/bookshelf.html
new file mode 100644
index 000000000..485622373
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.html
@@ -0,0 +1,32 @@
+
+
+
+
+ 阅读书架
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/web/bookshelf.js b/app/src/main/assets/web/bookshelf.js
new file mode 100644
index 000000000..22c8ff87e
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.js
@@ -0,0 +1,161 @@
+var $ = document.querySelector.bind(document)
+ , $$ = document.querySelectorAll.bind(document)
+ , $c = document.createElement.bind(document)
+ , randomImg = "http://acg.bakayun.cn/randbg.php?t=dfzh"
+ , randomImg2 = "http://img.xjh.me/random_img.php"
+ , books
+ ;
+
+var formatTime = value => {
+ return new Date(value).toLocaleString('zh-CN', {
+ hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
+ }).replace(/\//g, "-");
+};
+
+var apiMap = {
+ getBookshelf: "/getBookshelf",
+ getChapterList: "/getChapterList",
+ getBookContent: "/getBookContent",
+ saveBook: "/saveBook"
+};
+
+var apiAddress = (apiName, url) => {
+ let address = $('#address').value || window.location.host;
+ if (!(/^http|^\/\//).test(address)) {
+ address = "//" + address;
+ }
+ if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) {
+ address += ":1122";
+ }
+ localStorage.setItem('address', address);
+ return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
+};
+
+var init = () => {
+ $('#allcontent').classList.remove("read");
+ $('#books').innerHTML = "";
+ fetch(apiAddress("getBookshelf"), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(getBookshelf.errorMsg);
+ return;
+ }
+ books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);
+ books.forEach(book => {
+ let bookDiv = $c("div");
+ let img = $c("img");
+ img.src = book.bookInfoBean.coverUrl || randomImg;
+ img.setAttribute("data-series-num", book.serialNumber);
+ bookDiv.appendChild(img);
+ bookDiv.innerHTML += `
+ 书名: | ${book.bookInfoBean.name} |
+ 作者: | ${book.bookInfoBean.author} |
+ 阅读: | ${book.durChapterName} ${formatTime(book.finalDate)} |
+ 更新: | ${book.lastChapterName} ${formatTime(book.finalRefreshData)} |
+ 来源: | ${book.bookInfoBean.origin} |
+
`;
+ $('#books').appendChild(bookDiv);
+ });
+ $$('#books img').forEach(bookImg =>
+ bookImg.addEventListener("click", () => {
+ $('#allcontent').classList.add("read");
+ var book = books[bookImg.getAttribute("data-series-num")];
+ $("#info").innerHTML = `
+ 来源:${book.bookInfoBean.origin}
+ 书名:${book.bookInfoBean.name}
+ 作者:${book.bookInfoBean.author}
+ 阅读章节:${book.durChapterName}
+ 阅读时间:${formatTime(book.finalDate)}
+ 最新章节:${book.lastChapterName}
+ 检查时间:${formatTime(book.finalRefreshData)}
+ 简介:${book.bookInfoBean.introduce.trim().replace(/\n/g, "
")}
`;
+ window.location.hash = "";
+ window.location.hash = "#info";
+ $("#content").innerHTML = "章节列表加载中...";
+ $("#chapter").innerHTML = "";
+ fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(data.errorMsg);
+ $("#content").innerHTML = "章节列表加载失败!";
+ return;
+ }
+
+ data.data.forEach(chapter => {
+ let ch = $c("button");
+ ch.setAttribute("data-url", chapter.durChapterUrl);
+ ch.setAttribute("title", chapter.durChapterName);
+ ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName;
+ $("#chapter").appendChild(ch);
+ });
+ $('#chapter').scrollTop = 0;
+ $("#content").innerHTML = "章节列表加载完成!";
+ });
+
+ }));
+ });
+};
+
+$("#back").addEventListener("click", () => {
+ if (window.location.hash === "#content") {
+ window.location.hash = "#chapter";
+ } else if (window.location.hash === "#chapter") {
+ window.location.hash = "#info";
+ } else {
+ $('#allcontent').classList.remove("read");
+ }
+});
+
+$("#refresh").addEventListener("click", init);
+
+$('#hidebooks').addEventListener("click", () => {
+ $("#books").classList.toggle("hide");
+ $(".nav").classList.toggle("hide");
+ $("#allcontent").classList.toggle("allscreen");
+});
+
+$('#top').addEventListener("click", () => {
+ window.location.hash = "";
+ window.location.hash = "#info";
+});
+
+$('#showchapter').addEventListener("click", () => {
+ window.location.hash = "";
+ window.location.hash = "#chapter";
+});
+
+$('#chapter').addEventListener("click", (e) => {
+ if (e.target.tagName === "BUTTON") {
+ var url = e.target.getAttribute("data-url");
+ var name = e.target.getAttribute("title");
+ if (!url) {
+ alert("未取得章节地址");
+ }
+ $("#content").innerHTML = "" + name + " 加载中...
";
+ fetch(apiAddress("getBookContent", url), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(data.errorMsg);
+ $("#content").innerHTML = "" + name + " 加载失败!
";
+ return;
+ }
+ var content = data.data.trim().split("\n\n");
+ if (content.length === 2) {
+ $("#content").innerHTML = `${content[0]}
(全文 ${content[1].length} 字)
` + content[1].trim().replace(/\n/g, "
");
+ } else {
+ $("#content").innerHTML = `${name || e.target.innerHTML}
(全文 ${data.data.length} 字)
` + data.data.trim().replace(/\n/g, "
");
+ }
+ window.location.hash = "";
+ window.location.hash = "#content";
+ });
+ }
+});
+
+$('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host);
+if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {
+ $('#address').value = localStorage.getItem('address');
+}
+init();
diff --git a/app/src/main/assets/web/favicon.ico b/app/src/main/assets/web/favicon.ico
new file mode 100644
index 000000000..c3d5e6bc0
Binary files /dev/null and b/app/src/main/assets/web/favicon.ico differ
diff --git a/app/src/main/assets/web/index.css b/app/src/main/assets/web/index.css
new file mode 100644
index 000000000..a3e76ba06
--- /dev/null
+++ b/app/src/main/assets/web/index.css
@@ -0,0 +1,148 @@
+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,
+.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;
+}
\ No newline at end of file
diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html
new file mode 100644
index 000000000..2c47adb7f
--- /dev/null
+++ b/app/src/main/assets/web/index.html
@@ -0,0 +1,306 @@
+
+
+
+
+
+ 书源编辑器v3.8
+
+
+
+
+
+
+
+
书源基础信息
+
+
+
+
+
书籍发现规则
+
+
+
+
+
+
+
+
+
+
书籍搜索规则
+
+
+
+
+
+
+
+
+
+
+
书籍详情规则
+
+
+
+
+
+
+
+
+
目录列表规则
+
+
+
+
+
正文阅读规则
+
+
+
其它规则
+
+
+
+
+
+
+
+
+
+
+
编辑书源
+
调试书源
+
书源列表
+
帮助信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/web/index.js b/app/src/main/assets/web/index.js
new file mode 100644
index 000000000..9463800ce
--- /dev/null
+++ b/app/src/main/assets/web/index.js
@@ -0,0 +1,362 @@
+// 简化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}`;
+ }
+ }
+}
+// 创建书源规则容器对象
+const RuleJSON = (() => {
+ let ruleJson = {};
+ $$('.rules textarea').forEach(item => ruleJson[item.id] = '');
+// for (let item of $$('.rules textarea')) ruleJson[item.id] = '';
+ ruleJson.serialNumber = 0;
+ ruleJson.weight = 0;
+ ruleJson.enable = true;
+ 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 ``;
+}
+// 缓存规则列表
+var RuleSources = [];
+if (localStorage.getItem('RuleSources')) {
+ RuleSources = JSON.parse(localStorage.getItem('RuleSources'));
+ 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() {
+ Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value);
+ RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber);
+ RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
+ RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
+ return RuleJSON;
+}
+// 将书源对象填充到书源表单
+function json2rule(RuleEditor) {
+ Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : '');
+}
+// 记录操作过程
+var course = { "old": [], "now": {}, "new": [] };
+if (localStorage.getItem('course')) {
+ course = JSON.parse(localStorage.getItem('course'));
+ json2rule(course.now);
+}
+else {
+ course.now = rule2json();
+ window.localStorage.setItem('course', 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('course', JSON.stringify(course));
+}
+function undo() {
+ course = JSON.parse(localStorage.getItem('course'));
+ if (course.old.length > 0) {
+ course.new.push(course.now);
+ course.now = course.old.pop();
+ localStorage.setItem('course', JSON.stringify(course));
+ json2rule(course.now);
+ }
+}
+function redo() {
+ course = JSON.parse(localStorage.getItem('course'));
+ if (course.new.length > 0) {
+ course.old.push(course.now);
+ course.now = course.new.pop();
+ localStorage.setItem('course', 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}
${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(`/saveSources`, 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(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length} 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}`);
+ }
+ else {
+ alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length} 条`);
+ }
+ }
+ else {
+ alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`);
+ }
+ }).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); });
+ thisNode.setAttribute('class', '');
+ })();
+ return;
+ case 'pull':
+ showTab('书源列表');
+ (async () => {
+ await HttpGet(`/getSources`).then(json => {
+ if (json.isSuccess) {
+ $('#RuleList').innerHTML = ''
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));
+ RuleSources.forEach(item => {
+ $('#RuleList').innerHTML += newRule(item);
+ });
+ alert(`成功拉取 ${RuleSources.length} 条书源`);
+ }
+ else {
+ alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`);
+ }
+ }).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\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(`/saveSources`, saveRule).then(sResult => {
+ if (sResult.isSuccess) {
+ let sKey = DebugKey.value ? DebugKey.value : '我的';
+ $('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
+ let ws = new WebSocket(`${wsOrigin}/sourceDebug`);
+ ws.onopen = () => {
+ ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
+ };
+ ws.onmessage = (msg) => {
+ DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data);
+ if (msg.data == 'finish') setRule(saveRule[0]);
+ };
+ ws.onerror = (err) => {
+ throw `${err.data}`;
+ }
+ ws.onclose = () => {
+ thisNode.setAttribute('class', '');
+ DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`);
+ }
+ } else throw `${sResult.errorMsg}`;
+ }).catch(err => {
+ DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
+ thisNode.setAttribute('class', '');
+ });
+ return;
+ case 'accept':
+ (async () => {
+ let saveRule = [rule2json()];
+ await HttpPost(`/saveSources`, saveRule).then(json => {
+ alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
+ setRule(saveRule[0]);
+ }).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\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);
+ }
+});
+
+// 列表规则更改事件
+$('#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('RuleSources', 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('RuleSources', 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('RuleSources', 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(`/deleteSources`, deleteSources).then(json => {
+ if (json.isSuccess) {
+ let selectNode = document.getElementById(selectRuleUrl).parentNode;
+ selectNode.parentNode.removeChild(selectNode);
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));
+ if ($('#bookSourceUrl').value == selectRuleUrl) {
+ $$('.rules textarea').forEach(item => { item.value = '' });
+ todo();
+ }
+ console.log(deleteSources);
+ console.log(`以上书源已删除!`)
+ }
+ }).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); });
+ }
+ break;
+ case 'ClrAll':
+ if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) {
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));
+ $('#RuleList').innerHTML = ''
+ }
+ break;
+ default:
+ }
+});
\ No newline at end of file