web写源添加订阅源

pull/1395/head
gedoor 3 years ago
parent fe1d2720a0
commit 6e79fb489c
  1. 7
      app/src/main/assets/web/bookSource/index.html
  2. 42
      app/src/main/assets/web/bookSource/index.js
  3. 150
      app/src/main/assets/web/rssSource/index.css
  4. 242
      app/src/main/assets/web/rssSource/index.html
  5. 488
      app/src/main/assets/web/rssSource/index.js
  6. 12
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  7. 2
      app/src/main/java/io/legado/app/api/controller/BookSourceController.kt
  8. 83
      app/src/main/java/io/legado/app/api/controller/RssSourceController.kt
  9. 2
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  10. 18
      app/src/main/java/io/legado/app/web/HttpServer.kt

@ -80,7 +80,7 @@
</div>
<div>
<div>校验文字:</div>
<textarea rows="3" id="checkKeyWord" class="base" title="checkKeyWord"
<textarea rows="3" id="checkKeyWord" class="ruleSearch" title="checkKeyWord"
placeholder="校验关键字"></textarea>
</div>
<div>
@ -253,6 +253,11 @@
<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="ruleToc_isPay"
placeholder="章节是否为已购买 (规则结果为Bool)"></textarea>
</div>
<div>
<div>章节信息:</div>
<textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"

@ -70,8 +70,8 @@ function newRule(rule) {
}
// 缓存规则列表
var RuleSources = [];
if (localStorage.getItem('RuleSources')) {
RuleSources = JSON.parse(localStorage.getItem('RuleSources'));
if (localStorage.getItem('BookSources')) {
RuleSources = JSON.parse(localStorage.getItem('BookSources'));
RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
}
// 页面加载完成事件
@ -218,36 +218,36 @@ function json2rule(RuleEditor) {
}
// 记录操作过程
var course = { "old": [], "now": {}, "new": [] };
if (localStorage.getItem('course')) {
course = JSON.parse(localStorage.getItem('course'));
if (localStorage.getItem('bookSourceCourse')) {
course = JSON.parse(localStorage.getItem('bookSourceCourse'));
json2rule(course.now);
}
else {
course.now = rule2json();
window.localStorage.setItem('course', JSON.stringify(course));
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('course', JSON.stringify(course));
localStorage.setItem('bookSourceCourse', JSON.stringify(course));
}
function undo() {
course = JSON.parse(localStorage.getItem('course'));
course = JSON.parse(localStorage.getItem('bookSourceCourse'));
if (course.old.length > 0) {
course.new.push(course.now);
course.now = course.old.pop();
localStorage.setItem('course', JSON.stringify(course));
localStorage.setItem('bookSourceCourse', JSON.stringify(course));
json2rule(course.now);
}
}
function redo() {
course = JSON.parse(localStorage.getItem('course'));
course = JSON.parse(localStorage.getItem('bookSourceCourse'));
if (course.new.length > 0) {
course.old.push(course.now);
course.now = course.new.pop();
localStorage.setItem('course', JSON.stringify(course));
localStorage.setItem('bookSourceCourse', JSON.stringify(course));
json2rule(course.now);
}
}
@ -275,7 +275,7 @@ $('.menu').addEventListener('click', e => {
case 'push':
$$('#RuleList>label>div').forEach(item => { item.className = ''; });
(async () => {
await HttpPost(`/saveSources`, RuleSources).then(json => {
await HttpPost(`/saveBookSources`, RuleSources).then(json => {
if (json.isSuccess) {
let okData = json.data;
if (Array.isArray(okData)) {
@ -303,10 +303,10 @@ $('.menu').addEventListener('click', e => {
case 'pull':
showTab('书源列表');
(async () => {
await HttpGet(`/getSources`).then(json => {
await HttpGet(`/getBookSources`).then(json => {
if (json.isSuccess) {
$('#RuleList').innerHTML = ''
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));
localStorage.setItem('BookSources', JSON.stringify(RuleSources = json.data));
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
@ -349,7 +349,7 @@ $('.menu').addEventListener('click', e => {
let DebugInfos = $('#DebugConsole');
function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
let saveRule = [rule2json()];
HttpPost(`/saveSources`, saveRule).then(sResult => {
HttpPost(`/saveBookSources`, saveRule).then(sResult => {
if (sResult.isSuccess) {
let sKey = DebugKey.value ? DebugKey.value : '我的';
$('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
@ -377,7 +377,7 @@ $('.menu').addEventListener('click', e => {
case 'accept':
(async () => {
let saveRule = [rule2json()];
await HttpPost(`/saveSources`, saveRule).then(json => {
await HttpPost(`/saveBookSources`, saveRule).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}`); });
@ -426,7 +426,7 @@ $('#RuleList').addEventListener('click', e => {
if (editRule.bookSourceUrl == '') return;
if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
setRule(editRule);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
localStorage.setItem('BookSources', JSON.stringify(RuleSources));
});
// 处理列表按钮事件
$('.tab3>.titlebar').addEventListener('click', e => {
@ -448,7 +448,7 @@ $('.tab3>.titlebar').addEventListener('click', e => {
let newSources = [];
newSources.push(...fileJson);
if (window.confirm(`如何处理导入的书源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources));
localStorage.setItem('BookSources', JSON.stringify(RuleSources = newSources));
$('#RuleList').innerHTML = ''
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
@ -457,7 +457,7 @@ $('.tab3>.titlebar').addEventListener('click', e => {
else {
newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
RuleSources.push(...newSources);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
localStorage.setItem('BookSources', JSON.stringify(RuleSources));
newSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
@ -490,11 +490,11 @@ $('.tab3>.titlebar').addEventListener('click', e => {
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 => {
HttpPost(`/deleteBookSources`, deleteSources).then(json => {
if (json.isSuccess) {
let selectNode = document.getElementById(selectRuleUrl).parentNode;
selectNode.parentNode.removeChild(selectNode);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));
localStorage.setItem('BookSources', JSON.stringify(RuleSources = laveSources));
if ($('#bookSourceUrl').value == selectRuleUrl) {
$$('.rules textarea').forEach(item => { item.value = '' });
todo();
@ -507,7 +507,7 @@ $('.tab3>.titlebar').addEventListener('click', e => {
break;
case 'ClrAll':
if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));
localStorage.setItem('BookSources', JSON.stringify(RuleSources = []));
$('#RuleList').innerHTML = ''
}
break;

@ -0,0 +1,150 @@
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;
}

@ -0,0 +1,242 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>阅读3.0书源编辑器_V4.0</title>
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="index.css"/>
</head>
<body>
<div class="editor">
<div class="setbox">
<div>
<a href="../index.html">←主页</a>
</div>
<div class="rules">
<div><b>基本</b></div>
<div>
<div>源域名 :</div>
<textarea rows="1" id="sourceUrl" class="base" title="sourceUrl"
placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
</div>
<div>
<div>源名称 :</div>
<textarea rows="1" id="sourceName" class="base" title="sourceName"
placeholder="&lt;必填&gt;源名称"></textarea>
</div>
<div>
<div>图标  :</div>
<textarea rows="1" id="sourceIcon" class="base" title="sourceIcon"
placeholder="&lt;选填&gt;图标"></textarea>
</div>
<div>
<div>源分组 :</div>
<textarea rows="1" id="sourceGroup" class="base" title="sourceGroup"
placeholder="&lt;选填&gt;描述书源的特征信息"></textarea>
</div>
<div>
<div>源注释 :</div>
<textarea rows="1" id="sourceComment" class="base" title="sourceComment"
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="sortUrl" class="base" title="sortUrl"
placeholder="&lt;选填&gt;例:&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
</div>
<p></p>
<div><b>列表规则</b></div>
<div>
<div>列表样式:</div>
<textarea rows="3" id="articleStyle" class="base" title="articleStyle"
placeholder="列表样式:0,1,2"></textarea>
</div>
<div>
<div>列表规则:</div>
<textarea rows="3" id="ruleArticles" class="base" title="ruleArticles"
placeholder="列表规则 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>标题规则:</div>
<textarea rows="1" id="ruleTitle" class="base" title="ruleTitle"
placeholder="选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>时间规则:</div>
<textarea rows="1" id="rulePubDate" class="base" title="rulePubDate"
placeholder="发表时间 (规则结果为String)"></textarea>
</div>
<div>
<div>翻页规则:</div>
<textarea rows="1" id="ruleNextPage" class="base" title="ruleNextPage"
placeholder="下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea>
</div>
<p></p>
<div><b>WebView规则</b></div>
<div>
<div>加载url :</div>
<textarea rows="1" id="loadWithBaseUrl" class="base" title="loadWithBaseUrl"
placeholder="是否加载url (启用: true 关闭: false (可选,默认true))"></textarea>
</div>
<div>
<div>启用Js :</div>
<textarea rows="1" id="enableJs" class="base" title="enableJs"
placeholder="是否启用Js (启用: true 关闭: false (可选,默认true))"></textarea>
</div>
<div>
<div>描述规则:</div>
<textarea rows="1" id="ruleDescription" class="base" title="ruleDescription"
placeholder="rss内容 (规则结果为String)"></textarea>
</div>
<div>
<div>图片Url :</div>
<textarea rows="1" id="ruleImage" class="base" title="ruleImage"
placeholder="图片rul规则 (规则结果为url)"></textarea>
</div>
<div>
<div>原文链接:</div>
<textarea rows="1" id="ruleLink" class="base" title="ruleLink"
placeholder="原文链接规则 (规则结果为url)"></textarea>
</div>
<div>
<div>内容规则:</div>
<textarea rows="1" id="ruleContent" class="base" title="ruleContent"
placeholder="内容规则 (规则结果为String)"></textarea>
</div>
<div>
<div>内容样式:</div>
<textarea rows="1" id="style" class="base" title="style"
placeholder="内容样式 (css样式)"></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="customOrder" class="base" title="customOrder"
placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></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>

@ -0,0 +1,488 @@
// 简化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 = {};
// 基本以及其他
$$('.rules .base').forEach(item => ruleJson[item.title] = '');
ruleJson.customOrder = 0;
ruleJson.enabled = 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 `<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('RssSources')) {
RuleSources = JSON.parse(localStorage.getItem('RssSources'));
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('rssSourceCourse')) {
course = JSON.parse(localStorage.getItem('rssSourceCourse'));
json2rule(course.now);
}
else {
course.now = rule2json();
window.localStorage.setItem('rssSourceCourse', 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('rssSourceCourse', JSON.stringify(course));
}
function undo() {
course = JSON.parse(localStorage.getItem('rssSourceCourse'));
if (course.old.length > 0) {
course.new.push(course.now);
course.now = course.old.pop();
localStorage.setItem('rssSourceCourse', JSON.stringify(course));
json2rule(course.now);
}
}
function redo() {
course = JSON.parse(localStorage.getItem('rssSourceCourse'));
if (course.new.length > 0) {
course.old.push(course.now);
course.now = course.new.pop();
localStorage.setItem('rssSourceCourse', 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(`/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(`批量推送书源到「阅读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(`/getSources`).then(json => {
if (json.isSuccess) {
$('#RuleList').innerHTML = ''
localStorage.setItem('RssSources', 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(`/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) => {
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(`/saveSources`, saveRule).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('RssSources', 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('RssSources', 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('RssSources', 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('RssSources', 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('RssSources', JSON.stringify(RuleSources = []));
$('#RuleList').innerHTML = ''
}
break;
default:
}
});

@ -11,7 +11,7 @@ import android.database.MatrixCursor
import android.net.Uri
import com.google.gson.Gson
import io.legado.app.api.controller.BookController
import io.legado.app.api.controller.SourceController
import io.legado.app.api.controller.BookSourceController
import java.util.*
/**
@ -51,7 +51,7 @@ class ReaderProvider : ContentProvider() {
): Int {
if (sMatcher.match(uri) < 0) return -1
when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.DeleteSources -> SourceController.deleteSources(selection)
RequestCode.DeleteSources -> BookSourceController.deleteSources(selection)
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
)
@ -65,13 +65,13 @@ class ReaderProvider : ContentProvider() {
if (sMatcher.match(uri) < 0) return null
when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.SaveSource -> values?.let {
SourceController.saveSource(values.getAsString(postBodyKey))
BookSourceController.saveSource(values.getAsString(postBodyKey))
}
RequestCode.SaveBook -> values?.let {
BookController.saveBook(values.getAsString(postBodyKey))
}
RequestCode.SaveSources -> values?.let {
SourceController.saveSources(values.getAsString(postBodyKey))
BookSourceController.saveSources(values.getAsString(postBodyKey))
}
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
@ -95,8 +95,8 @@ class ReaderProvider : ContentProvider() {
map["path"] = arrayListOf(it)
}
return if (sMatcher.match(uri) < 0) null else when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.GetSource -> SimpleCursor(SourceController.getSource(map))
RequestCode.GetSources -> SimpleCursor(SourceController.sources)
RequestCode.GetSource -> SimpleCursor(BookSourceController.getSource(map))
RequestCode.GetSources -> SimpleCursor(BookSourceController.sources)
RequestCode.GetBookshelf -> SimpleCursor(BookController.bookshelf)
RequestCode.GetBookContent -> SimpleCursor(BookController.getBookContent(map))
RequestCode.RefreshToc -> SimpleCursor(BookController.refreshToc(map))

@ -10,7 +10,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.msg
object SourceController {
object BookSourceController {
val sources: ReturnData
get() {

@ -0,0 +1,83 @@
package io.legado.app.api.controller
import android.text.TextUtils
import io.legado.app.api.ReturnData
import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.msg
object RssSourceController {
val sources: ReturnData
get() {
val source = appDb.rssSourceDao.all
val returnData = ReturnData()
return if (source.isEmpty()) {
returnData.setErrorMsg("订阅源列表为空")
} else returnData.setData(source)
}
fun saveSource(postData: String?): ReturnData {
val returnData = ReturnData()
postData ?: return returnData.setErrorMsg("数据不能为空")
kotlin.runCatching {
val source = GSON.fromJsonObject<RssSource>(postData)
if (source != null) {
if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
returnData.setErrorMsg("源名称和URL不能为空")
} else {
appDb.rssSourceDao.insert(source)
returnData.setData("")
}
} else {
returnData.setErrorMsg("转换源失败")
}
}.onFailure {
returnData.setErrorMsg(it.msg)
}
return returnData
}
fun saveSources(postData: String?): ReturnData {
val okSources = arrayListOf<RssSource>()
kotlin.runCatching {
val source = GSON.fromJsonArray<RssSource>(postData)
if (source != null) {
for (rssSource in source) {
if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
continue
}
appDb.rssSourceDao.insert(rssSource)
okSources.add(rssSource)
}
}
}
return ReturnData().setData(okSources)
}
fun getSource(parameters: Map<String, List<String>>): ReturnData {
val url = parameters["url"]?.firstOrNull()
val returnData = ReturnData()
if (url.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书源地址")
}
val source = appDb.rssSourceDao.getByKey(url)
?: return returnData.setErrorMsg("未找到源,请检查源地址")
return returnData.setData(source)
}
fun deleteSources(postData: String?): ReturnData {
kotlin.runCatching {
GSON.fromJsonArray<RssSource>(postData)?.let {
it.forEach { source ->
appDb.rssSourceDao.delete(source)
}
}
}
return ReturnData().setData("已执行"/*okSources*/)
}
}

@ -27,8 +27,8 @@ data class RssSource(
var loginCheckJs: String? = null, //登录检测js
var sortUrl: String? = null,
var singleUrl: Boolean = false,
var articleStyle: Int = 0,
//列表规则
var articleStyle: Int = 0, //列表样式,0,1,2
var ruleArticles: String? = null,
var ruleNextPage: String? = null,
var ruleTitle: String? = null,

@ -5,7 +5,8 @@ import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
import io.legado.app.api.ReturnData
import io.legado.app.api.controller.BookController
import io.legado.app.api.controller.SourceController
import io.legado.app.api.controller.BookSourceController
import io.legado.app.api.controller.RssSourceController
import io.legado.app.web.utils.AssetsWeb
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@ -38,11 +39,14 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
val postData = files["postData"]
returnData = when (uri) {
"/saveSource" -> SourceController.saveSource(postData)
"/saveSources" -> SourceController.saveSources(postData)
"/saveBookSource" -> BookSourceController.saveSource(postData)
"/saveBookSources" -> BookSourceController.saveSources(postData)
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
"/saveBook" -> BookController.saveBook(postData)
"/deleteSources" -> SourceController.deleteSources(postData)
"/addLocalBook" -> BookController.addLocalBook(session.parameters)
"/saveRssSource" -> RssSourceController.saveSource(postData)
"/saveRssSources" -> RssSourceController.saveSources(postData)
"/deleteRssSources" -> RssSourceController.deleteSources(postData)
else -> null
}
}
@ -50,13 +54,15 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
val parameters = session.parameters
returnData = when (uri) {
"/getSource" -> SourceController.getSource(parameters)
"/getSources" -> SourceController.sources
"/getBookSource" -> BookSourceController.getSource(parameters)
"/getBookSources" -> BookSourceController.sources
"/getBookshelf" -> BookController.bookshelf
"/getChapterList" -> BookController.getChapterList(parameters)
"/refreshToc" -> BookController.refreshToc(parameters)
"/getBookContent" -> BookController.getBookContent(parameters)
"/cover" -> BookController.getCover(parameters)
"/getRssSource" -> RssSourceController.getSource(parameters)
"/getRssSources" -> RssSourceController.sources
else -> null
}
}

Loading…
Cancel
Save