Compare commits
No commits in common. 'master' and 'record2021' have entirely different histories.
master
...
record2021
@ -1,53 +0,0 @@ |
||||
name: BUG提交 / BUG Report |
||||
description: 提交BUG / Report bugs to developers |
||||
labels: ["BUG"] |
||||
body: |
||||
- type: checkboxes |
||||
attributes: |
||||
label: 确认 / Assignments |
||||
description: 提交issue请确保完成以下前提,否则该issue可能被忽略 / Make sure you read checkboxs below |
||||
options: |
||||
- label: 搜索现有issues,不存在相似或相关的issue / No similar or related issues |
||||
required: true |
||||
- label: 最新[测试版](https://kunfei.lanzoui.com/b0f810h4b)依然存在此问题 / Latest beta app does not work |
||||
required: true |
||||
- label: 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件等无关 / Make sure your machine is not touched by hook frameworks, plugins etc |
||||
required: true |
||||
|
||||
- type: textarea |
||||
attributes: |
||||
label: 问题描述 / Describe Bugs |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: 复现步骤 / How to reproduce |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: 日志提交 / Relevant log output |
||||
description: 阅读日志位于我的-关于-崩溃日志、书架-右上角-日志,或者自行使用log工具抓取日志 |
||||
|
||||
- type: input |
||||
attributes: |
||||
label: 阅读版本 / Legado version |
||||
placeholder: "3.22.110823" |
||||
validations: |
||||
required: true |
||||
- type: input |
||||
attributes: |
||||
label: Android版本 / Android version |
||||
placeholder: "Android 12" |
||||
validations: |
||||
required: true |
||||
- type: input |
||||
attributes: |
||||
label: 机型 / Model |
||||
placeholder: "Redmi K30 Pro" |
||||
validations: |
||||
required: true |
||||
|
||||
- type: textarea |
||||
attributes: |
||||
label: 其他信息 / Additions |
@ -0,0 +1,19 @@ |
||||
--- |
||||
name: "[FeatureRequest] 功能请求模板" |
||||
about: 提交你希望能够在阅读中增加的功能 |
||||
title: "[Feature Request] " |
||||
labels: '需求' |
||||
assignees: '' |
||||
--- |
||||
|
||||
### 功能描述(请清晰的、详细的描述你想要的功能) |
||||
> |
||||
|
||||
### 期望实现方式(阅读应该如何实现该功能) |
||||
> |
||||
|
||||
### 附加信息(其他的与功能相关的附加信息) |
||||
> |
||||
|
||||
### 效果演示(可以手绘一些草图,或者提供可借鉴的图片) |
||||
> |
@ -1,32 +0,0 @@ |
||||
name: 功能请求 |
||||
description: 提交你希望能够在阅读中增加的功能 |
||||
labels: ["需求"] |
||||
body: |
||||
- type: checkboxes |
||||
attributes: |
||||
label: 确认 |
||||
description: 提交issue请确保完成以下前提,否则该issue可能被忽略 |
||||
options: |
||||
- label: 搜索现有issues,不存在相似或相关的issue |
||||
required: true |
||||
|
||||
- type: textarea |
||||
attributes: |
||||
label: 功能描述 |
||||
placeholder: 请清晰的、详细的描述你想要的功能 |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: 期望实现方式 |
||||
placeholder: 阅读应该如何实现该功能 |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: 附加信息 |
||||
placeholder: 其他的与功能相关的附加信息 |
||||
- type: textarea |
||||
attributes: |
||||
label: 效果演示 |
||||
placeholder: 可以手绘一些草图,或者提供可借鉴的图片 |
@ -1,40 +0,0 @@ |
||||
# To get started with Dependabot version updates, you'll need to specify which |
||||
# package ecosystems to update and where the package manifests are located. |
||||
# Please see the documentation for all configuration options: |
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates |
||||
|
||||
version: 2 |
||||
registries: |
||||
maven-google: |
||||
type: maven-repository |
||||
url: https://maven.google.com |
||||
password: dummy |
||||
username: dummy |
||||
maven-central: |
||||
type: maven-repository |
||||
url: https://repo1.maven.org/maven2/ |
||||
password: dummy |
||||
username: dummy |
||||
maven-jitpack: |
||||
type: maven-repository |
||||
url: https://jitpack.io |
||||
password: dummy |
||||
username: dummy |
||||
|
||||
updates: |
||||
- package-ecosystem: gradle |
||||
directory: "app" |
||||
schedule: |
||||
interval: "daily" |
||||
registries: "*" |
||||
- package-ecosystem: gradle |
||||
directory: "epublib" |
||||
schedule: |
||||
interval: "daily" |
||||
registries: "*" |
||||
|
||||
# Maintain dependencies for GitHub Actions |
||||
- package-ecosystem: "github-actions" |
||||
directory: "/" |
||||
schedule: |
||||
interval: "daily" |
@ -1,36 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
echo "fetch release info from https://chromiumdash.appspot.com ..." |
||||
|
||||
branch="Stable" |
||||
|
||||
lastest_cronet_version=`curl -s "https://chromiumdash.appspot.com/fetch_releases?channel=$branch&platform=Android&num=1&offset=0" | jq .[0].version -r` |
||||
echo "lastest_cronet_version: $lastest_cronet_version" |
||||
#lastest_cronet_version=100.0.4845.0 |
||||
lastest_cronet_main_version=${lastest_cronet_version%%\.*}.0.0.0 |
||||
|
||||
function checkVersionExit() { |
||||
local jar_url="https://storage.googleapis.com/chromium-cronet/android/$lastest_cronet_version/Release/cronet/cronet_api.jar" |
||||
statusCode=$(curl -s -I -w %{http_code} "$jar_url" -o /dev/null) |
||||
if [ $statusCode == "404" ];then |
||||
echo "storage.googleapis.com return 404 for cronet $lastest_cronet_version" |
||||
exit |
||||
fi |
||||
} |
||||
|
||||
path=$GITHUB_WORKSPACE/gradle.properties |
||||
current_cronet_version=`cat $path | grep CronetVersion | sed s/CronetVersion=//` |
||||
echo "current_cronet_version: $current_cronet_version" |
||||
|
||||
if [[ $current_cronet_version == $lastest_cronet_version ]];then |
||||
echo "cronet is already latest" |
||||
else |
||||
checkVersionExit |
||||
sed -i s/CronetVersion=.*/CronetVersion=$lastest_cronet_version/ $path |
||||
sed -i s/CronetMainVersion=.*/CronetMainVersion=$lastest_cronet_main_version/ $path |
||||
sed "15a* 更新cronet: $lastest_cronet_version" -i $GITHUB_WORKSPACE/app/src/main/assets/updateLog.md |
||||
echo "start download latest cronet" |
||||
chmod +x gradlew |
||||
./gradlew app:downloadCronet |
||||
fi |
||||
|
@ -1,44 +0,0 @@ |
||||
name: Update Cronet |
||||
|
||||
on: |
||||
schedule: |
||||
- cron: 0 0 * * * |
||||
workflow_dispatch: |
||||
|
||||
jobs: |
||||
build: |
||||
runs-on: ubuntu-latest |
||||
if: ${{ github.actor == 'gedoor' }} |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
fetch-depth: 1 |
||||
|
||||
- name: Set up Gradle |
||||
uses: gradle/gradle-build-action@v2 |
||||
|
||||
- name: Check Cronet Updates |
||||
run: | |
||||
echo "获取cronet最新版本" |
||||
source .github/scripts/cronet.sh |
||||
echo "PR_TITLE=Bump cronet from $current_cronet_version to $lastest_cronet_version " >> $GITHUB_ENV |
||||
echo "PR_BODY=Changes in the [Git log](https://chromium.googlesource.com/chromium/src/+log/$current_cronet_version..$lastest_cronet_version)" >> $GITHUB_ENV |
||||
|
||||
- name: Create Pull Request |
||||
uses: peter-evans/create-pull-request@v4 |
||||
continue-on-error: true |
||||
with: |
||||
token: ${{ secrets.ACTIONS_TOKEN }} |
||||
title: ${{ env.PR_TITLE }} |
||||
commit-message: | |
||||
${{ env.PR_TITLE }} |
||||
- ${{ env.PR_BODY }} |
||||
body: ${{ env.PR_BODY }} |
||||
branch: cronet |
||||
delete-branch: true |
||||
add-paths: | |
||||
*cronet*jar |
||||
*cronet.json |
||||
*updateLog.md |
||||
gradle.properties |
||||
|
@ -1,26 +0,0 @@ |
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. |
||||
# |
||||
# You can adjust the behavior by modifying this file. |
||||
# For more information, see: |
||||
# https://github.com/actions/stale |
||||
name: closeStaleIssue |
||||
|
||||
on: |
||||
schedule: |
||||
- cron: '30 1 * * *' |
||||
|
||||
jobs: |
||||
stale: |
||||
|
||||
runs-on: ubuntu-latest |
||||
if: ${{ github.actor == 'gedoor' }} |
||||
permissions: |
||||
issues: write |
||||
|
||||
steps: |
||||
- uses: actions/stale@v7 |
||||
with: |
||||
repo-token: ${{ secrets.GITHUB_TOKEN }} |
||||
stale-issue-message: '由于长期没有状态更新,该问题将于5天后自动关闭。如有需要可重新打开。' |
||||
days-before-stale: 30 |
||||
days-before-close: 5 |
@ -1 +0,0 @@ |
||||
legado |
@ -1,6 +1,5 @@ |
||||
<component name="InspectionProjectProfileManager"> |
||||
<settings> |
||||
<option name="PROJECT_PROFILE" value="Default" /> |
||||
<option name="USE_PROJECT_PROFILE" value="false" /> |
||||
<version value="1.0" /> |
||||
</settings> |
||||
|
@ -1,11 +1,4 @@ |
||||
**2022/10/02** |
||||
**2021/08/09** |
||||
|
||||
* 更新cronet: 106.0.5249.79 |
||||
* 正文选择菜单朗读按钮长按可切换朗读选择内容和从选择开始处一直朗读 |
||||
* 源编辑输入框设置最大行数12,在行数特别多的时候更容易滚动到其它输入 |
||||
* 修复某些情况下无法搜索到标题的bug,净化规则较多的可能会降低搜索速度 by Xwite |
||||
* 修复文件类书源换源后阅读bug by Xwite |
||||
* Cronet 支持DnsHttpsSvcb by g2s20150909 |
||||
* 修复web进度同步问题 by 821938089 |
||||
* 启用混淆以减小app大小 有bug请带日志反馈 |
||||
* 其它一些优化 |
||||
1. 修复选择文字不能选择单个文字的bug |
||||
2. |
||||
|
@ -1,149 +0,0 @@ |
||||
# [English](English.md) [中文](README.md) |
||||
|
||||
[![icon_android](https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/icon_android.png)](https://play.google.com/store/apps/details?id=io.legado.play.release) |
||||
<a href="https://jb.gg/OpenSourceSupport" target="_blank"> |
||||
<img width="24" height="24" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_gl=1*135yekd*_ga*OTY4Mjg4NDYzLjE2Mzk0NTE3MzQ.*_ga_9J976DJZ68*MTY2OTE2MzM5Ny4xMy4wLjE2NjkxNjMzOTcuNjAuMC4w&_ga=2.257292110.451256242.1669085120-968288463.1639451734" alt="idea"/> |
||||
</a> |
||||
<div align="center"> |
||||
<img width="125" height="125" src="https://github.com/gedoor/legado/raw/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" alt="legado"/> |
||||
|
||||
Legado / 开源阅读 |
||||
<br> |
||||
<a href="https://gedoor.github.io" target="_blank">gedoor.github.io</a> / <a href="https://www.legado.top/" target="_blank">legado.top</a> |
||||
<br> |
||||
Legado is a free and open source novel reader for Android. |
||||
</div> |
||||
|
||||
[![](https://img.shields.io/badge/-Contents:-696969.svg)](#contents) [![](https://img.shields.io/badge/-Function-F5F5F5.svg)](#Function-) [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-) [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-) [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-) [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-) [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-) [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-) |
||||
|
||||
>New user? |
||||
> |
||||
>The software does not provide content, you need to add it manually, such as importing book sources, etc. |
||||
>Take a look at [official help documentation](https://www.yuque.com/legado/wiki),Maybe there's an answer you need inside. |
||||
|
||||
# Function [![](https://img.shields.io/badge/-Function-F5F5F5.svg)](#Function-) |
||||
|
||||
You can customize the book source, set your own rules, and capture web page data. The rules are simple and easy to understand. There are rules in the software. List bookshelf, grid bookshelf switch freely. The book source rules support search and discovery, and all the functions of finding books and reading books are all customized, making it easier to find books. |
||||
* Custom ebook sources, set your own rules to capture web data, the rules are simple and easy to understand, the software has a rule description. |
||||
* eBook sources rules support search and discovery, all find books and read books function all custom, find books more convenient. |
||||
* Schedule updating your library for new chapters. |
||||
* Online reading from web sources that can be imported in bulk |
||||
* Local reading of Auto-download episodes. |
||||
* Local reading of TXT or EPUB files |
||||
* ebook Wishlist |
||||
* Big text viewer. You can open eBook and txt in 1GB size |
||||
* Automatic text replacement for removing ad in content |
||||
* List bookshelf, grid bookshelf free to switch. |
||||
* Subscription content, you can subscribe to any content you want to see, see what you want to see |
||||
* A configurable reader with fonts, background, page transitions mode and other settings |
||||
* Timer. Set interval time to listen ebook, time up, ebook turn off completely. |
||||
* TTS book reader. tts can optionally be install“smartvoice-4.1.0” or ”Speech Services by Google“ Give your baby a storybook to listen to and teach your baby to talk, |
||||
* Dark mode and E-Ink mode support and Web service support |
||||
* Create backups to local or WebDav server |
||||
* Decentralization web3 |
||||
* Support replacement purification, it is very convenient to remove the content of advertisement replacement. |
||||
* Support local TXT, EPUB reading, manual browsing, intelligent scanning. |
||||
* Support highly customized reading interface, switch font, color, background, line spacing, paragraph spacing, bold, simplified and traditional conversion. |
||||
* Support multiple page turning modes, covering, emulating, sliding, scrolling, etc. |
||||
|
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# Download [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-) |
||||
|
||||
#### Android |
||||
|
||||
* [Releases](https://github.com/gedoor/legado/releases/latest) |
||||
* [Google play - $1.99](https://play.google.com/store/apps/details?id=io.legado.play.release) |
||||
* [Coolapk](https://www.coolapk.com/apk/io.legado.app.release) |
||||
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b) |
||||
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release) |
||||
|
||||
|
||||
#### IOS |
||||
|
||||
* Stopped(No release) - [Github](https://github.com/gedoor/YueDuFlutter) |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# Community [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-) |
||||
|
||||
#### Telegram |
||||
|
||||
[![Telegram-group](https://img.shields.io/badge/Telegram-group-blue)](https://t.me/yueduguanfang) [![Telegram-channel](https://img.shields.io/badge/Telegram-channel-blue)](https://t.me/legado_channels) |
||||
|
||||
#### Discord |
||||
|
||||
[![Discord](https://img.shields.io/discord/560731361414086666?color=%235865f2&label=Discord)](https://discord.gg/VtUfRyzRXn) |
||||
|
||||
#### Other |
||||
|
||||
https://www.yuque.com/legado/wiki/community |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# API [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-) |
||||
|
||||
* Legado 3.0 The API is provided in 2 ways: `Web way` and `Content Provider way`. You can call it yourself as needed in [here](api.md). |
||||
* One-click import by url recall reading, url format: legado://import/{path}?src={url} |
||||
* Path Type: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig,addToBookshelf |
||||
* path type explanation: Book source, subscription source, replacement rules, local txt novel directory rules, online reading engine, theme, reading layout, add to bookshelf |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# Other [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-) |
||||
|
||||
##### Disclaimers |
||||
|
||||
https://gedoor.github.io/Disclaimer |
||||
|
||||
##### Legado 3.0 |
||||
|
||||
* [eBook sources rules](https://alanskycn.gitee.io/teachme) |
||||
* [Update Log](/app/src/main/assets/updateLog.md) |
||||
* [Help Documentation](/app/src/main/assets/help/appHelp.md) |
||||
* [web bookshelf](https://github.com/gedoor/legado_web_bookshelf) |
||||
* [web source editor](https://github.com/gedoor/legado_web_source_editor) |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# Grateful [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-) |
||||
|
||||
> * org.jsoup:jsoup |
||||
> * cn.wanghaomiao:JsoupXpath |
||||
> * com.jayway.jsonpath:json-path |
||||
> * com.github.gedoor:rhino-android |
||||
> * com.squareup.okhttp3:okhttp |
||||
> * com.github.bumptech.glide:glide |
||||
> * org.nanohttpd:nanohttpd |
||||
> * org.nanohttpd:nanohttpd-websocket |
||||
> * cn.bingoogolapple:bga-qrcode-zxing |
||||
> * com.jaredrummler:colorpicker |
||||
> * org.apache.commons:commons-text |
||||
> * io.noties.markwon:core |
||||
> * io.noties.markwon:image-glide |
||||
> * com.hankcs:hanlp |
||||
> * com.positiondev.epublib:epublib-core |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
||||
|
||||
# Interface [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-) |
||||
|
||||
<img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg" width="270"> |
||||
<img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B4.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg" width="270"> |
||||
|
||||
<a href="#readme"> |
||||
<img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right"> |
||||
</a> |
@ -1,14 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!--suppress XmlUnusedNamespaceDeclaration --> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:tools="http://schemas.android.com/tools"> |
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> |
||||
<uses-permission |
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" |
||||
tools:ignore="ScopedStorage" /> |
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" |
||||
tools:ignore="ScopedStorage" /> |
||||
|
||||
</manifest> |
@ -1,17 +0,0 @@ |
||||
package io.legado.app.help |
||||
|
||||
import androidx.annotation.Keep |
||||
import io.legado.app.utils.IntentType |
||||
|
||||
@Keep |
||||
@Suppress("unused") |
||||
object AppIntentType : IntentType.TypeInterface { |
||||
|
||||
override fun from(path: String?): String? { |
||||
return when (path?.substringAfterLast(".")?.lowercase()) { |
||||
"apk" -> "application/vnd.android.package-archive" |
||||
else -> null |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,46 +0,0 @@ |
||||
package io.legado.app.help |
||||
|
||||
import androidx.annotation.Keep |
||||
import io.legado.app.constant.AppConst |
||||
import io.legado.app.exception.NoStackTraceException |
||||
import io.legado.app.help.coroutine.Coroutine |
||||
import io.legado.app.help.http.newCallStrResponse |
||||
import io.legado.app.help.http.okHttpClient |
||||
import io.legado.app.utils.jsonPath |
||||
import io.legado.app.utils.readString |
||||
import kotlinx.coroutines.CoroutineScope |
||||
|
||||
@Keep |
||||
@Suppress("unused") |
||||
object AppUpdateGitHub: AppUpdate.AppUpdateInterface { |
||||
|
||||
override fun check( |
||||
scope: CoroutineScope, |
||||
): Coroutine<AppUpdate.UpdateInfo> { |
||||
return Coroutine.async(scope) { |
||||
val lastReleaseUrl = "https://api.github.com/repos/gedoor/legado/releases/latest" |
||||
val body = okHttpClient.newCallStrResponse { |
||||
url(lastReleaseUrl) |
||||
}.body |
||||
if (body.isNullOrBlank()) { |
||||
throw NoStackTraceException("获取新版本出错") |
||||
} |
||||
val rootDoc = jsonPath.parse(body) |
||||
val tagName = rootDoc.readString("$.tag_name") |
||||
?: throw NoStackTraceException("获取新版本出错") |
||||
if (tagName > AppConst.appInfo.versionName) { |
||||
val updateBody = rootDoc.readString("$.body") |
||||
?: throw NoStackTraceException("获取新版本出错") |
||||
val downloadUrl = rootDoc.readString("$.assets[0].browser_download_url") |
||||
?: throw NoStackTraceException("获取新版本出错") |
||||
val fileName = rootDoc.readString("$.assets[0].name") |
||||
?: throw NoStackTraceException("获取新版本出错") |
||||
return@async AppUpdate.UpdateInfo(tagName, updateBody, downloadUrl, fileName) |
||||
} else { |
||||
throw NoStackTraceException("已是最新版本") |
||||
} |
||||
}.timeout(10000) |
||||
} |
||||
|
||||
|
||||
} |
@ -1,66 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import androidx.annotation.Keep |
||||
import okhttp3.RequestBody |
||||
import okio.Buffer |
||||
import org.chromium.net.UploadDataProvider |
||||
import org.chromium.net.UploadDataSink |
||||
import java.io.IOException |
||||
import java.nio.ByteBuffer |
||||
|
||||
@Keep |
||||
class BodyUploadProvider(private val body: RequestBody) : UploadDataProvider(), AutoCloseable { |
||||
|
||||
private val buffer = Buffer() |
||||
|
||||
@Volatile |
||||
private var filled: Boolean = false |
||||
|
||||
init { |
||||
fillBuffer() |
||||
} |
||||
|
||||
private fun fillBuffer() { |
||||
try { |
||||
buffer.clear() |
||||
body.writeTo(buffer) |
||||
buffer.flush() |
||||
} catch (e: IOException) { |
||||
e.printStackTrace() |
||||
} |
||||
} |
||||
|
||||
@Throws(IOException::class) |
||||
override fun getLength(): Long { |
||||
return body.contentLength() |
||||
} |
||||
|
||||
@Throws(IOException::class) |
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) { |
||||
if (!filled) { |
||||
fillBuffer() |
||||
} |
||||
check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" } |
||||
var read: Int |
||||
var bytesRead = 0 |
||||
while (bytesRead == 0) { |
||||
read = buffer.read(byteBuffer) |
||||
bytesRead += read |
||||
} |
||||
uploadDataSink.onReadSucceeded(false) |
||||
} |
||||
|
||||
@Throws(IOException::class) |
||||
override fun rewind(uploadDataSink: UploadDataSink) { |
||||
check(body.isOneShot()) { "Okhttp RequestBody is oneShot" } |
||||
filled = false |
||||
fillBuffer() |
||||
uploadDataSink.onRewindSucceeded() |
||||
} |
||||
|
||||
@Throws(IOException::class) |
||||
override fun close() { |
||||
buffer.close() |
||||
super.close() |
||||
} |
||||
} |
@ -1,112 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import androidx.annotation.Keep |
||||
import io.legado.app.utils.printOnDebug |
||||
import kotlinx.coroutines.runBlocking |
||||
import kotlinx.coroutines.suspendCancellableCoroutine |
||||
import kotlinx.coroutines.withTimeout |
||||
import okhttp3.* |
||||
import okhttp3.internal.http.receiveHeaders |
||||
import org.chromium.net.UrlRequest |
||||
import org.chromium.net.UrlResponseInfo |
||||
import java.io.IOException |
||||
import kotlin.coroutines.resume |
||||
import kotlin.coroutines.resumeWithException |
||||
|
||||
@Keep |
||||
@Suppress("unused") |
||||
class CronetCoroutineInterceptor(private val cookieJar: CookieJar) : Interceptor { |
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response { |
||||
if (chain.call().isCanceled()) { |
||||
throw IOException("Canceled") |
||||
} |
||||
val original: Request = chain.request() |
||||
//Cronet未初始化 |
||||
return if (!CronetLoader.install() || cronetEngine == null) { |
||||
chain.proceed(original) |
||||
} else try { |
||||
val builder: Request.Builder = original.newBuilder() |
||||
//移除Keep-Alive,手动设置会导致400 BadRequest |
||||
builder.removeHeader("Keep-Alive") |
||||
builder.removeHeader("Accept-Encoding") |
||||
if (cookieJar != CookieJar.NO_COOKIES) { |
||||
val cookieStr = getCookie(original.url) |
||||
//设置Cookie |
||||
if (cookieStr.length > 3) { |
||||
builder.addHeader("Cookie", cookieStr) |
||||
} |
||||
} |
||||
|
||||
val newReq = builder.build() |
||||
val timeout = chain.call().timeout().timeoutNanos() / 1000000 |
||||
runBlocking() { |
||||
if (timeout > 0) { |
||||
withTimeout(timeout) { |
||||
proceedWithCronet(newReq, chain.call()).also { response -> |
||||
cookieJar.receiveHeaders(newReq.url, response.headers) |
||||
} |
||||
} |
||||
} else { |
||||
proceedWithCronet(newReq, chain.call()).also { response -> |
||||
cookieJar.receiveHeaders(newReq.url, response.headers) |
||||
} |
||||
} |
||||
} |
||||
|
||||
} catch (e: Exception) { |
||||
//不能抛出错误,抛出错误会导致应用崩溃 |
||||
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 |
||||
if (!e.message.toString().contains("ERR_CERT_", true) |
||||
&& !e.message.toString().contains("ERR_SSL_", true) |
||||
) { |
||||
e.printOnDebug() |
||||
} |
||||
chain.proceed(original) |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
private suspend fun proceedWithCronet(request: Request, call: Call): Response = |
||||
suspendCancellableCoroutine<Response> { coroutine -> |
||||
|
||||
val callBack = object : AbsCallBack(originalRequest = request, mCall = call) { |
||||
override fun waitForDone(urlRequest: UrlRequest): Response { |
||||
TODO("Not yet implemented") |
||||
} |
||||
|
||||
override fun onError(error: IOException) { |
||||
coroutine.resumeWithException(error) |
||||
} |
||||
|
||||
override fun onSuccess(response: Response) { |
||||
coroutine.resume(response) |
||||
} |
||||
|
||||
override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) { |
||||
super.onCanceled(request, info) |
||||
coroutine.cancel() |
||||
} |
||||
|
||||
|
||||
} |
||||
|
||||
val req = buildRequest(request, callBack)?.also { it.start() } |
||||
coroutine.invokeOnCancellation { |
||||
req?.cancel() |
||||
} |
||||
|
||||
|
||||
} |
||||
|
||||
|
||||
/** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */ |
||||
private fun getCookie(url: HttpUrl): String = buildString { |
||||
val cookies = cookieJar.loadForRequest(url) |
||||
cookies.forEachIndexed { index, cookie -> |
||||
if (index > 0) append("; ") |
||||
append(cookie.name).append('=').append(cookie.value) |
||||
} |
||||
} |
||||
} |
@ -1,77 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import android.os.Build |
||||
import androidx.annotation.Keep |
||||
import io.legado.app.utils.printOnDebug |
||||
import okhttp3.* |
||||
import okhttp3.internal.http.receiveHeaders |
||||
import java.io.IOException |
||||
|
||||
@Keep |
||||
@Suppress("unused") |
||||
class CronetInterceptor(private val cookieJar: CookieJar) : Interceptor { |
||||
|
||||
@Throws(IOException::class) |
||||
override fun intercept(chain: Interceptor.Chain): Response { |
||||
if (chain.call().isCanceled()) { |
||||
throw IOException("Canceled") |
||||
} |
||||
val original: Request = chain.request() |
||||
//Cronet未初始化 |
||||
return if (!CronetLoader.install() || cronetEngine == null) { |
||||
chain.proceed(original) |
||||
} else try { |
||||
val builder: Request.Builder = original.newBuilder() |
||||
//移除Keep-Alive,手动设置会导致400 BadRequest |
||||
builder.removeHeader("Keep-Alive") |
||||
builder.removeHeader("Accept-Encoding") |
||||
if (cookieJar != CookieJar.NO_COOKIES) { |
||||
val cookieStr = getCookie(original.url) |
||||
//设置Cookie |
||||
if (cookieStr.length > 3) { |
||||
builder.addHeader("Cookie", cookieStr) |
||||
} |
||||
} |
||||
|
||||
val newReq = builder.build() |
||||
proceedWithCronet(newReq, chain.call())?.let { response -> |
||||
//从Response 中保存Cookie到CookieJar |
||||
cookieJar.receiveHeaders(newReq.url, response.headers) |
||||
response |
||||
} ?: chain.proceed(original) |
||||
} catch (e: Exception) { |
||||
//不能抛出错误,抛出错误会导致应用崩溃 |
||||
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 |
||||
if (!e.message.toString().contains("ERR_CERT_", true) |
||||
&& !e.message.toString().contains("ERR_SSL_", true) |
||||
) { |
||||
e.printOnDebug() |
||||
} |
||||
chain.proceed(original) |
||||
} |
||||
|
||||
} |
||||
|
||||
private fun proceedWithCronet(request: Request, call: Call): Response? { |
||||
val callBack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
NewCallBack(request, call) |
||||
} else { |
||||
OldCallback(request, call) |
||||
} |
||||
buildRequest(request, callBack)?.runCatching { |
||||
return callBack.waitForDone(this) |
||||
} |
||||
return null |
||||
} |
||||
|
||||
|
||||
/** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */ |
||||
private fun getCookie(url: HttpUrl): String = buildString { |
||||
val cookies = cookieJar.loadForRequest(url) |
||||
cookies.forEachIndexed { index, cookie -> |
||||
if (index > 0) append("; ") |
||||
append(cookie.name).append('=').append(cookie.value) |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,75 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import androidx.annotation.Keep |
||||
import okhttp3.RequestBody |
||||
import okio.BufferedSource |
||||
import okio.Pipe |
||||
import okio.buffer |
||||
import org.chromium.net.UploadDataProvider |
||||
import org.chromium.net.UploadDataSink |
||||
import java.io.IOException |
||||
import java.nio.ByteBuffer |
||||
import java.util.concurrent.ExecutorService |
||||
|
||||
/** |
||||
* 用于上传大型文件 |
||||
* |
||||
* @property body |
||||
* @property executorService |
||||
*/ |
||||
@Keep |
||||
class LargeBodyUploadProvider( |
||||
private val body: RequestBody, |
||||
private val executorService: ExecutorService |
||||
) : UploadDataProvider(), AutoCloseable { |
||||
private val pipe = Pipe(BUFFER_SIZE.toLong()) |
||||
private var source: BufferedSource = pipe.source.buffer() |
||||
|
||||
@Volatile |
||||
private var filled: Boolean = false |
||||
override fun getLength(): Long { |
||||
return body.contentLength() |
||||
} |
||||
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) { |
||||
if (!filled) { |
||||
fillBuffer() |
||||
} |
||||
check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" } |
||||
var read: Int |
||||
var bytesRead = 0 |
||||
while (bytesRead <= 0) { |
||||
read = source.read(byteBuffer) |
||||
bytesRead += read |
||||
} |
||||
uploadDataSink.onReadSucceeded(false) |
||||
} |
||||
|
||||
@Synchronized |
||||
private fun fillBuffer() { |
||||
executorService.submit { |
||||
try { |
||||
val writeSink = pipe.sink.buffer() |
||||
filled = true |
||||
body.writeTo(writeSink) |
||||
writeSink.flush() |
||||
} catch (e: IOException) { |
||||
e.printStackTrace() |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
override fun rewind(p0: UploadDataSink?) { |
||||
check(body.isOneShot()) { "Okhttp RequestBody is OneShot" } |
||||
filled = false |
||||
fillBuffer() |
||||
} |
||||
|
||||
override fun close() { |
||||
// pipe.cancel() |
||||
// source.close() |
||||
super.close() |
||||
} |
||||
} |
@ -1,48 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import android.os.Build |
||||
import androidx.annotation.Keep |
||||
import androidx.annotation.RequiresApi |
||||
import okhttp3.Call |
||||
import okhttp3.Request |
||||
import okhttp3.Response |
||||
import org.chromium.net.UrlRequest |
||||
import java.io.IOException |
||||
import java.util.concurrent.CompletableFuture |
||||
import java.util.concurrent.TimeUnit |
||||
|
||||
@Keep |
||||
@RequiresApi(api = Build.VERSION_CODES.N) |
||||
class NewCallBack(originalRequest: Request, mCall: Call) : AbsCallBack(originalRequest, mCall) { |
||||
|
||||
private val responseFuture = CompletableFuture<Response>() |
||||
|
||||
@Throws(IOException::class) |
||||
override fun waitForDone(urlRequest: UrlRequest): Response { |
||||
urlRequest.start() |
||||
return if (mCall.timeout().timeoutNanos() > 0) { |
||||
responseFuture.get(mCall.timeout().timeoutNanos(), TimeUnit.NANOSECONDS) |
||||
} else { |
||||
return responseFuture.get() |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 当发生错误时,通知子类终止阻塞抛出错误 |
||||
* @param error |
||||
*/ |
||||
override fun onError(error: IOException) { |
||||
responseFuture.completeExceptionally(error) |
||||
} |
||||
|
||||
/** |
||||
* 请求成功后,通知子类结束阻塞,返回response |
||||
* @param response |
||||
*/ |
||||
override fun onSuccess(response: Response) { |
||||
responseFuture.complete(response) |
||||
} |
||||
|
||||
|
||||
} |
@ -1,57 +0,0 @@ |
||||
package io.legado.app.lib.cronet |
||||
|
||||
import android.os.ConditionVariable |
||||
import androidx.annotation.Keep |
||||
import okhttp3.Call |
||||
import okhttp3.Request |
||||
import okhttp3.Response |
||||
import org.chromium.net.UrlRequest |
||||
import java.io.IOException |
||||
|
||||
@Keep |
||||
class OldCallback(originalRequest: Request, mCall: Call) : AbsCallBack(originalRequest, mCall) { |
||||
|
||||
private val mResponseCondition = ConditionVariable() |
||||
private var mException: IOException? = null |
||||
|
||||
@Throws(IOException::class) |
||||
override fun waitForDone(urlRequest: UrlRequest): Response { |
||||
//获取okhttp call的完整请求的超时时间 |
||||
val timeOutMs: Long = mCall.timeout().timeoutNanos() / 1000000 |
||||
urlRequest.start() |
||||
if (timeOutMs > 0) { |
||||
mResponseCondition.block(timeOutMs) |
||||
} else { |
||||
mResponseCondition.block() |
||||
} |
||||
//ConditionVariable 正常open或者超时open后,检查urlRequest是否完成 |
||||
if (!urlRequest.isDone) { |
||||
urlRequest.cancel() |
||||
mException = IOException("Cronet timeout after wait " + timeOutMs + "ms") |
||||
} |
||||
|
||||
if (mException != null) { |
||||
throw mException as IOException |
||||
} |
||||
return mResponse |
||||
} |
||||
|
||||
/** |
||||
* 当发生错误时,通知子类终止阻塞抛出错误 |
||||
* @param error |
||||
*/ |
||||
override fun onError(error: IOException) { |
||||
mException = error |
||||
mResponseCondition.open() |
||||
} |
||||
|
||||
/** |
||||
* 请求成功后,通知子类结束阻塞,返回response |
||||
* @param response |
||||
*/ |
||||
override fun onSuccess(response: Response) { |
||||
mResponseCondition.open() |
||||
} |
||||
|
||||
|
||||
} |
@ -1,114 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="contributors" |
||||
android:summary="@string/contributors_summary" |
||||
android:title="@string/contributors" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="update_log" |
||||
android:title="@string/update_log" |
||||
app:allowDividerAbove="false" |
||||
app:allowDividerBelow="false" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="check_update" |
||||
android:title="@string/check_update" |
||||
app:allowDividerAbove="false" |
||||
app:allowDividerBelow="false" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.PreferenceCategory |
||||
android:title="@string/contact" |
||||
app:allowDividerAbove="true" |
||||
app:allowDividerBelow="false" |
||||
app:iconSpaceReserved="false" |
||||
app:layout="@layout/view_preference_category"> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="gzGzh" |
||||
android:summary="@string/official_account" |
||||
android:title="@string/follow_official_account" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="qqChannel" |
||||
android:title="@string/join_qq_channel" |
||||
android:summary="@string/qq_channel_summary" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="qq" |
||||
android:summary="@string/click_to_apply" |
||||
android:title="@string/join_qq_group" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="mail" |
||||
android:summary="@string/email" |
||||
android:title="@string/send_mail" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="git" |
||||
android:summary="@string/this_github_url" |
||||
android:title="@string/git_hub" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="discord" |
||||
android:summary="@string/discord_url" |
||||
android:title="Discord" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="tg" |
||||
android:summary="@string/tg_url" |
||||
android:title="TG" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="sourceRuleSummary" |
||||
android:summary="@string/source_rule_url" |
||||
android:title="@string/source_rule_s" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="home_page" |
||||
android:summary="@string/home_page_url" |
||||
android:title="@string/home_page" |
||||
app:allowDividerAbove="false" |
||||
app:allowDividerBelow="false" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
</io.legado.app.lib.prefs.PreferenceCategory> |
||||
|
||||
<io.legado.app.lib.prefs.PreferenceCategory |
||||
android:title="@string/other" |
||||
app:allowDividerAbove="true" |
||||
app:allowDividerBelow="false" |
||||
app:iconSpaceReserved="false" |
||||
app:layout="@layout/view_preference_category"> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="crashLog" |
||||
android:title="@string/crash_log" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="license" |
||||
android:title="@string/license" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
<io.legado.app.lib.prefs.Preference |
||||
android:key="disclaimer" |
||||
android:title="@string/disclaimer" |
||||
app:iconSpaceReserved="false" /> |
||||
|
||||
</io.legado.app.lib.prefs.PreferenceCategory> |
||||
|
||||
</androidx.preference.PreferenceScreen> |
@ -1,20 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?><!--suppress XmlUnusedNamespaceDeclaration --> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:tools="http://schemas.android.com/tools"> |
||||
|
||||
<uses-permission |
||||
android:name="android.permission.REQUEST_INSTALL_PACKAGES" |
||||
tools:node="remove" /> |
||||
<uses-permission |
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" |
||||
tools:ignore="ScopedStorage" |
||||
tools:node="remove" /> |
||||
<uses-permission |
||||
android:name="android.permission.READ_EXTERNAL_STORAGE" |
||||
tools:node="remove" /> |
||||
<uses-permission |
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE" |
||||
tools:ignore="ScopedStorage" |
||||
tools:node="remove" /> |
||||
|
||||
</manifest> |
@ -1,32 +0,0 @@ |
||||
[ |
||||
{ |
||||
"customOrder": 2, |
||||
"enableJs": true, |
||||
"enabled": true, |
||||
"singleUrl": true, |
||||
"sourceGroup": "legado", |
||||
"sourceIcon": "http://mmbiz.qpic.cn/mmbiz_png/hpfMV8hEuL2eS6vnCxvTzoOiaCAibV6exBzJWq9xMic9xDg3YXAick87tsfafic0icRwkQ5ibV0bJ84JtSuxhPuEDVquA/0?wx_fmt=png", |
||||
"sourceName": "小说拾遗", |
||||
"sourceUrl": "snssdk1128://user/profile/562564899806367" |
||||
}, |
||||
{ |
||||
"customOrder": 3, |
||||
"enableJs": true, |
||||
"enabled": true, |
||||
"singleUrl": true, |
||||
"sourceGroup": "legado", |
||||
"sourceIcon": "https://cdn.jsdelivr.net/gh/mgz0227/meowcloud/icon.png", |
||||
"sourceName": "Meow云", |
||||
"sourceUrl": "https://pan.miaogongzi.net" |
||||
}, |
||||
{ |
||||
"customOrder": 4, |
||||
"enableJs": true, |
||||
"enabled": true, |
||||
"singleUrl": true, |
||||
"sourceGroup": "legado", |
||||
"sourceIcon": "https://cdn.jsdelivr.net/gh/gedoor/legado@master/app/src/main/res/mipmap-hdpi/ic_launcher.png", |
||||
"sourceName": "烏雲净化", |
||||
"sourceUrl": "https://www.lanzoux.com/b0bw8jwoh" |
||||
} |
||||
] |
@ -1 +1 @@ |
||||
{"x86":"6cdf8e3e9ced5ad7ae506245e3231d74","armeabi-v7a":"c4c855612c3cdead96702a41571f2ed3","x86_64":"afa8e7db75f19f939c9eca3b0aaa46d7","arm64-v8a":"77228743b00c1a7ee0ec722229a7c6ba","version":"108.0.5359.128"} |
||||
{"arm64-v8a":"690c212d9bbad4b09b9e1ba450b273bb","armeabi-v7a":"4dbb88e5229abef7d84138218772f872","x86":"3f2421e040147da48abb07cfc6c7c87e","x86_64":"730a71ef4f03a27d1c8c8a77e7d09ff5","version":"96.0.4664.104"} |
@ -1,5 +0,0 @@ |
||||
{ |
||||
"enable": true, |
||||
"searchUrl": "https://api.yousuu.com/api/search?type=title&value={{key}}&page=1&highlight=0&from=search", |
||||
"coverRule": "@js:java.getString(\"$..books[?(@.author == '\" + book.author + \"')].cover\")" |
||||
} |
@ -1,5 +1,5 @@ |
||||
{ |
||||
"uploadUrl": "https://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", |
||||
"downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result", |
||||
"UploadUrl": "http://sy.miaogongzi.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", |
||||
"DownloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.miaogongzi.cc/shuyuan/' + result", |
||||
"summary": "有效期2天" |
||||
} |
@ -1,162 +0,0 @@ |
||||
[ |
||||
{ |
||||
"key": "@css:", |
||||
"value": "@css:", |
||||
"serialNo": 0 |
||||
}, |
||||
{ |
||||
"key": "<js>", |
||||
"value": "<js></js>", |
||||
"serialNo": 1 |
||||
}, |
||||
{ |
||||
"key": "{{}}", |
||||
"value": "{{}}", |
||||
"serialNo": 2 |
||||
}, |
||||
{ |
||||
"key": "##", |
||||
"value": "##", |
||||
"serialNo": 3 |
||||
}, |
||||
{ |
||||
"key": "&&", |
||||
"value": "&&", |
||||
"serialNo": 4 |
||||
}, |
||||
{ |
||||
"key": "%%", |
||||
"value": "%%", |
||||
"serialNo": 5 |
||||
}, |
||||
{ |
||||
"key": "||", |
||||
"value": "||", |
||||
"serialNo": 6 |
||||
}, |
||||
{ |
||||
"key": "//", |
||||
"value": "//", |
||||
"serialNo": 7 |
||||
}, |
||||
{ |
||||
"key": "\\", |
||||
"value": "\\", |
||||
"serialNo": 8 |
||||
}, |
||||
{ |
||||
"key": "$.", |
||||
"value": "$.", |
||||
"serialNo": 9 |
||||
}, |
||||
{ |
||||
"key": "@", |
||||
"value": "@", |
||||
"serialNo": 10 |
||||
}, |
||||
{ |
||||
"key": ":", |
||||
"value": ":", |
||||
"serialNo": 11 |
||||
}, |
||||
{ |
||||
"key": "class", |
||||
"value": "class", |
||||
"serialNo": 12 |
||||
}, |
||||
{ |
||||
"key": "text", |
||||
"value": "text", |
||||
"serialNo": 13 |
||||
}, |
||||
{ |
||||
"key": "href", |
||||
"value": "href", |
||||
"serialNo": 14 |
||||
}, |
||||
{ |
||||
"key": "textNodes", |
||||
"value": "textNodes", |
||||
"serialNo": 15 |
||||
}, |
||||
{ |
||||
"key": "ownText", |
||||
"value": "ownText", |
||||
"serialNo": 16 |
||||
}, |
||||
{ |
||||
"key": "all", |
||||
"value": "all", |
||||
"serialNo": 17 |
||||
}, |
||||
{ |
||||
"key": "html", |
||||
"value": "html", |
||||
"serialNo": 18 |
||||
}, |
||||
{ |
||||
"key": "[", |
||||
"value": "[", |
||||
"serialNo": 19 |
||||
}, |
||||
{ |
||||
"key": "]", |
||||
"value": "]", |
||||
"serialNo": 20 |
||||
}, |
||||
{ |
||||
"key": "<", |
||||
"value": "<", |
||||
"serialNo": 21 |
||||
}, |
||||
{ |
||||
"key": ">", |
||||
"value": ">", |
||||
"serialNo": 22 |
||||
}, |
||||
{ |
||||
"key": "#", |
||||
"value": "#", |
||||
"serialNo": 23 |
||||
}, |
||||
{ |
||||
"key": "!", |
||||
"value": "!", |
||||
"serialNo": 24 |
||||
}, |
||||
{ |
||||
"key": ".", |
||||
"value": ".", |
||||
"serialNo": 25 |
||||
}, |
||||
{ |
||||
"key": "+", |
||||
"value": "+", |
||||
"serialNo": 26 |
||||
}, |
||||
{ |
||||
"key": "-", |
||||
"value": "-", |
||||
"serialNo": 27 |
||||
}, |
||||
{ |
||||
"key": "*", |
||||
"value": "*", |
||||
"serialNo": 28 |
||||
}, |
||||
{ |
||||
"key": "/", |
||||
"value": "/", |
||||
"serialNo": 29 |
||||
}, |
||||
{ |
||||
"key": "=", |
||||
"value": "=", |
||||
"serialNo": 30 |
||||
}, |
||||
{ |
||||
"key": "useWebView", |
||||
"value": "{'webView': true}", |
||||
"serialNo": 31 |
||||
} |
||||
] |
@ -1,222 +1,190 @@ |
||||
# 帮助文档 |
||||
|
||||
【温馨提醒】 *本帮助可以在“**我的**”——右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* |
||||
## **新人必读** |
||||
|
||||
## 新人必读 |
||||
【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* |
||||
|
||||
### 1. 为什么第一次安装好之后什么东西都没有? |
||||
阅读只是一个转码工具,不提供内容,第一次安装 APP,需要自己手动导入书源,可以从公众号 **【开源阅读】**、QQ 群或酷安评论里获取由书友制作分享的书源。 |
||||
1. 为什么第一次安装好之后什么东西都没有? |
||||
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 |
||||
|
||||
### 2. 正文出现缺字漏字、内容缺失或排版错乱等情况,如何处理? |
||||
有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 |
||||
2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理? |
||||
* 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 |
||||
|
||||
### 3. 漫画源看书显示乱码,如何解决? |
||||
【异次元】和【阅读】是两个不同的软件,**两个软件的源并不通用**,请导入【阅读】的支持的漫画源! |
||||
3. 漫画源看书显示乱码,如何解决? |
||||
* 异次元和阅读是两个不同的软件,**两个软件的源并不通用**,请导入阅读的支持的漫画源! |
||||
|
||||
## 书源相关 |
||||
|
||||
### 1. 如何导入本地书源文件? |
||||
以导入 QQ 接收到的书源文件为例: |
||||
* 下载群文件里的书源文件; |
||||
* 打开【阅读】软件; |
||||
* 点击“**我的**”——“**书源管理**”; |
||||
* 点击右上角选择“**本地导入**”; |
||||
1. 如何导入本地书源文件? |
||||
* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入); |
||||
* 打开阅读软件; |
||||
* 我的 - 点击“书源管理”; |
||||
* 点击右上角选择“本地导入”; |
||||
* 左下角选择书源文件所在的路径; |
||||
* 点击书源文件导入; |
||||
* 导入后返回书源管理界面; |
||||
* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/ |
||||
|
||||
**【注】** |
||||
1. *新版 QQ 文件下载路径:`Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/`。* |
||||
2. *书源格式后缀有 .txt 和 .json,其中 .json 文件在某些情况下可能无法导入,需要修改后缀为 .txt 才可导入。* |
||||
|
||||
![QQ 导入书源](https://cdn.jsdelivr.net/gh/gedoor/gedoor.github.io@master/images/importSource.jpg) |
||||
![QQ导入书源](https://cdn.jsdelivr.net/gh/gedoor/gedoor.github.io@master/images/importSource.jpg) |
||||
|
||||
### 2. 如何新建大佬发的单独书源? |
||||
2. 如何新建大佬发的单独书源? |
||||
* 复制书源代码; |
||||
* 打开阅读软件; |
||||
* 点击“**我的**”——“**书源管理**”; |
||||
* 右上角“**⁝**”——“**+ 新建书源**”; |
||||
* 进入后点击右上角“**⁝**”——“**粘贴源**”; |
||||
* 粘贴完成后点击上方保存“**🖫**”按钮; |
||||
* 我的 - 点击“书源管理”; |
||||
* 右上角选择“新建书源”; |
||||
* 进入新建书源后点击右上角“粘贴源”; |
||||
* 粘贴书源完成后点击上方保存; |
||||
* 本次新建单独书源操作完成。 |
||||
* 注:如果书源有错误或者复制不全会显示格式错误,请重新复制。 |
||||
|
||||
**【注】** *如果书源有错误或者复制不全会显示格式错误,请重新复制。* |
||||
|
||||
### 3. 为什么导入 2.0 书源后无法阅读? |
||||
部分 2.0 书源并不适用于 3.0 版本的阅读,建议导入后进行筛选。 |
||||
3. 为什么导入2.0书源后无法阅读? |
||||
* 部分2.0书源并不适用于3.0版本的阅读,建议导入后进行筛选。 |
||||
|
||||
### 4. 【阅读】2.0 数据如何导入【阅读】3.0? |
||||
先对【阅读】2.0 的数据进行备份,然后进入【阅读】3.0,点击“**我的**”,选择“**备份与恢复**”,再点击“**导入旧版本数据**”。 |
||||
4. 阅读2.0数据如何导入阅读3.0? |
||||
* 先对阅读2.0的数据进行备份,然后进入阅读3.0,点击“我的”,选择“备份与恢复”,再点击“导入旧版本数据”。 |
||||
|
||||
### 5. 如何给朋友分享我的书源? |
||||
* 打开【阅读】软件; |
||||
5. 如何给朋友分享我的书源? |
||||
* 打开阅读软件; |
||||
* 点击备份; |
||||
* 打开手机系统自带的文件管理; |
||||
* 在手机内置存储根目录找到 `YueDu3.0` 文件夹; |
||||
* 找到 `myBookSource.json`,长按选择分享; |
||||
* 选择微信分享或者 QQ 分享; |
||||
* 打开手机自带的文件管理; |
||||
* 手机自带内存根目录找到YueDu3.0文件夹; |
||||
* 找到myBookSource.json长按选择分享; |
||||
* 选择微信分享或者QQ分享; |
||||
* 选择你要分享的好友点击发送; |
||||
* 好友接收后在手机内置存储根目录找到 `myBookSource.json` 文件; |
||||
* 复制该文件到手机内置存储根目录找到 `YueDu3.0` 文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹); |
||||
* 打开【阅读】软件点击恢复。 |
||||
|
||||
**【注】** |
||||
1. *备份路径如已修改过请在修改后的路径下查找书源文件。* |
||||
2. *Android 10 及以下版本系统,新版 QQ 文件接收路径在 `Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/`,旧版 QQ 文件接收路径则在 `Tencent/QQfile_recv/`;新版微信文件接收路径在 `Android/data/com.tencent.mobileqq/Tencent/MicroMsg/Download`,旧版微信文件接收路径则在 `Tencent/MicroMsg/Download`。* |
||||
3. *Android 11 及以上系统版本用户,由于系统限制,无法访问 `Android/data` 目录。* |
||||
|
||||
### 6. 效验书源显示失效就说明书源不能用了吗? |
||||
效验书源只是测试书源,可以做为参考,但失效了不代表书源不能用了。 |
||||
|
||||
### 7. “发现”和正版书源能不能使用? |
||||
发现和正版书源只能用来找书或看排行榜,不能用来看书,如需看书请切换书源。 |
||||
|
||||
### 8. 为什么书源这么多,“发现”里却只有一点点? |
||||
书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。 |
||||
|
||||
## 本地/WebDav远程书籍相关 |
||||
* 好友接收后在手机自带内存根目录找到myBookSource.json文件(最新版QQ 安卓10及以下版本在Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/,安卓11版本用户由于系统限制无法访问data目录,微信在Tencent/MicroMsg/Download); |
||||
* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹); |
||||
* 打开阅读软件点击恢复。 |
||||
* 注:备份路径如已修改过请在修改后的路径下查找书源文件。 |
||||
|
||||
### 1. 目前阅读支持哪些格式的本地书籍? |
||||
目前支持 TXT 和 EPUB 格式。 |
||||
6. 效验书源显示失效就说明书源不能用了吗? |
||||
* 效验书源只是测试书源,可以做个参考,失效了不代表书源不能用了。 |
||||
|
||||
### 2. 如何导入本地/WebDav远程书籍? |
||||
本地:在书架页面点击右上角“**⁝**”,选择“**添加本地**”,授予相关权限后即可导入本地书籍。也可在文件管理器中使用【阅读】打开相关书籍。 |
||||
7. 发现和正版书源能不能使用? |
||||
* 发现和正版书源只能用来找书,看排行榜,不能用来看书,如需看书请切换书源。 |
||||
|
||||
远程:在主页面点击右上角 “**⁝**”,选择 **WebDav书籍**,正确配置好后即可看到上传的远程书籍,点击 **加入书架** 按钮导入即可。 |
||||
8. 为什么书源这么多,发现里却只有一点点? |
||||
* 书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。 |
||||
|
||||
### 3. 如何上传本地书籍到 WebDav 远程? |
||||
长按本地书籍,进入书籍详情页,点击右上角 “**⁝**”,选择 **上传WebDav**,等待几秒后即可上传到远程。 |
||||
## 本地书籍相关 |
||||
1. 目前阅读支持哪些格式的本地书籍? |
||||
* 目前支持TXT、EPUB格式 |
||||
|
||||
或进入书籍缓存页面,点击右上角 “**⁝**”,选择 **导出到 WebDav**,在书籍导出时便可同时上传到远程。 |
||||
2. 如何导入本地书籍? |
||||
* 在书架页面点击右上角,选择“添加本地”,授予相关权限后即可导入本地书籍。也可在文件管理器中使用 **阅读** 打开相关书籍。 |
||||
|
||||
### 4. 导入 TXT 文件提示“LoadTocError”或“List is empty”是怎么回事? |
||||
* 请先去应用详情中确认是否授予了【阅读】“读写手机存储”的权限。 |
||||
3. 导入TXT文件提示“LoadTocError”或“List is empty”是怎么回事? |
||||
* 请先去应用详情中确认是否授予了阅读“读写手机存储”的权限。 |
||||
* 自动识别目录失败,可能是相关目录规则未开启,请点击右上角的换源按钮手动更换目录规则。 |
||||
* 如果尝试所有规则均无法识别,请在github上提交issue并附件相关txt文件,也可以发送邮件至i@qnmlgb.trade(标题:legado本地文件章节无法识别,内容对其具体情况进行简要说明,附件上传相关txt文件)。 |
||||
|
||||
如果尝试所有规则均无法识别,请在 GitHub 上提交 Issue 并附上相关 TXT 文件,也可以发送邮件至 i@qnmlgb.trade(标题:legado 本地文件章节无法识别;内容对其具体情况进行简要说明,附件上传相关 TXT 文件)。 |
||||
4. 如何下载书籍到本地? |
||||
* 把在线书籍加入到书架后,在书架页面点击右上角,选择“离线缓存”即可。 |
||||
|
||||
### 5. 如何下载书籍到本地? |
||||
把在线书籍加入到书架后,在书架页面点击右上角,选择“**离线缓存**”即可。 |
||||
|
||||
### 6. 如何自定义导出的 TXT 或 EPUB 文件名称? |
||||
* 点击“**离线缓存**“——”**导出文件名**“ |
||||
5. 如何自定义导出的txt/epub文件名称? |
||||
* 点击 **离线缓存** - **导出文件名**. |
||||
* 使用方法: |
||||
- 导出文件名支持 js 语法 |
||||
- 可用变量: name(书名)和 author(作者) |
||||
- 导出文件名支持js语法 |
||||
- 可用变量: name - 书名 author-作者 |
||||
- 示例: |
||||
> name + "作者:" + author |
||||
> {name+"作者:"+author} |
||||
- 导出文件名: |
||||
> Legado 是最好的在线阅读软件 作者: kunfei |
||||
> Legado是最好的在线阅读软件 作者: kunfei. |
||||
|
||||
**【注】** *name、author 等变量与字符串的拼接都需要在 JSON 上下文环境中进行,即必须使用 `{}` 将变量与字符串包裹起来。* |
||||
**注意:** name、author等变量与字符串的拼接都需要在js环境中进行,即必须使用{ } 将变量与字符串包裹起来. |
||||
|
||||
### 7. 为什么我打开本地的 TXT 文件,显示内容却是乱码? |
||||
部分编码在阅读上会识别错误,建议先用文本编辑器转换为常用的 UTF-8 格式。 |
||||
6. 为什么我打开本地的TXT文件,显示内容却是乱码? |
||||
* 部分编码在阅读上会识别错误,建议先用文本编辑器转换为常用的UTF-8格式。 |
||||
|
||||
### 8. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决? |
||||
点击右上角更换目录规则即可。 |
||||
7. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决? |
||||
* 点击右上角更换目录规则即可。 |
||||
|
||||
## 书籍界面相关 |
||||
|
||||
### 1. 如何刷新书架? |
||||
在书架界面下拉即可刷新。 |
||||
1. 如何刷新书架? |
||||
* 在书架界面下拉即可刷新。 |
||||
|
||||
### 2. 书架界面书籍右上角的红色或者灰色背景小数字代表什么? |
||||
红色代表书籍有更新,灰色代表无更新,数字代表未读章节。 |
||||
2. 书架界面书籍右上角的红色或者灰色背景小数字代表什么? |
||||
* 红色代表书籍有更新,灰色代表无更新,数字代表未读章节。 |
||||
|
||||
### 3. 如何查看书籍详情? |
||||
长按书籍即可查看。 |
||||
3. 如何查看书籍详情? |
||||
* 长按书籍。 |
||||
|
||||
### 4. 如何对书架上的书进行删除、切换书架的操作? |
||||
书籍详情页操作即可。 |
||||
4. 如何对书架上的书进行删除、切换书架的操作? |
||||
* 书籍详情页操作即可。 |
||||
|
||||
### 5. 如何禁止或允许某本书更新? |
||||
书籍详情页,点击右上角——“**允许更新**”。 |
||||
5. 如何禁止或允许某本书更新? |
||||
* 书籍详情页,点击右上角 - “允许更新”。 |
||||
|
||||
### 6. 如何更换小说封面、名字、作者或简介? |
||||
书籍详情页,点击右上角修改按钮。 |
||||
6. 如何更换小说封面、名字、作者或简介? |
||||
* 书籍详情页,点击右上角修改按钮。 |
||||
|
||||
### 7. 怎么使用自定义字体? |
||||
阅读界面——“**字体**”——点击右上角选择字体文件路径。 |
||||
7. 怎么使用自定义字体? |
||||
* 阅读界面 - 字体-点击右上角选择字体文件路径。 |
||||
|
||||
### 8. 目前支持哪些格式的字体文件? |
||||
目前支持 TTF 和 OTF 格式。 |
||||
8. 目前支持哪些格式的字体文件? |
||||
* 目前支持ttf、otf格式。 |
||||
|
||||
### 9. 书籍经常“正在加载中”怎么办? |
||||
在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。 |
||||
9. 书籍经常“正在加载中”怎么办? |
||||
* 在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。 |
||||
|
||||
### 10. 书籍内容只有标题,正文内容是路径怎么办? |
||||
通常是缓存路径引起的,更换缓存路径即可。 |
||||
10. 书籍内容只有标题,正文内容是路径怎么办? |
||||
* 通常是缓存路径引起的,更换缓存路径即可。 |
||||
|
||||
### 11. 看书时如遇到“目录为空”、“加载失败”或长串英文等情况怎么办? |
||||
在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。 |
||||
11. 看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办? |
||||
* 在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。 |
||||
|
||||
### 12. 为什么每一章的最后一页,阅读的文字和横线背景总是对不齐? |
||||
请在“**设置**”——“**文字底部对齐**”选项中关闭底部对齐,再调整排版。 |
||||
12. 为什么每一章的最后一页,阅读的文字和横线背景总是对不齐? |
||||
* 请在 设置-文字底部对齐 选项中关闭底部对齐,再调整排版。 |
||||
|
||||
### 13. 漫画源或图片章节只能看到第一页,如何解决? |
||||
请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的“**⁝**”按钮,在弹出的菜单中,选择“**翻页动画(本书)**”,将翻页动画更改为“**滚动**”。 |
||||
13. 漫画源或图片章节只能看到第一页,如何解决? |
||||
* 请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的 **⁝** 按钮,在弹出的菜单中,选择 **翻页动画(本书)**,将翻页动画更改为 **滚动**。 |
||||
|
||||
### 14. 阅读图片章节、漫画或 EPUB 插图时,图片被缩放到一页中,以至无法看清,如何处理? |
||||
* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考 Q13 中的方案将翻页动画更改为“**滚动**”。 |
||||
* 3.0 旧版可以点击书籍界面的章节标题进入“**编辑书源**”界面,在“**正文**”——“**图片样式**”中填入 *`full`*,保存更改,刷新当前章节即可。 |
||||
* 3.0 新版可以直接在书籍阅读界面点击右上角的“**⁝**”按钮,选择“**图片样式**”——***`full`***。 |
||||
14. 阅读图片章节、漫画或epub插图时,图片被缩放到一页中,以至无法看清,如何处理? |
||||
* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考Q13中的方案将翻页动画更改为**滚动** |
||||
* 3.0旧版可以点击书籍界面的章节标题进入 **编辑书源** 界面,在 正文-图片样式 中填入 *full*,保存更改,刷新当前章节即可。 |
||||
* 3.0新版可以直接在书籍阅读界面点击右上角的 **⁝** 按钮,选择 图片样式- *full*. |
||||
|
||||
|
||||
## 替换净化相关 |
||||
1. 替换净化是什么? |
||||
* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 |
||||
|
||||
### 1. 替换净化是什么? |
||||
替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 |
||||
|
||||
### 2. 如何自己填写净化替换规则? |
||||
* 第一行:替换规则名称。请根据自己需求对替换净化规则进行命名; |
||||
* 第二行:分组。净化规则的分组组别; |
||||
* 第三行:替换规则。填写需要被替换的内容; |
||||
* 第四行:替换为。填写想替换成的内容(如不填则默认表示删除第三行里填写的内容); |
||||
* 第五行:替换范围,选填书名或者源名。填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。 |
||||
|
||||
**【注】** *如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第三行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行网上搜索学习)。* |
||||
2. 如何自己填写净化替换规则? |
||||
* 第一行:替换规则名称 - 根据自己需求对替换净化规则进行命名; |
||||
* 第二行:分组 - 净化规则的分组组别; |
||||
* 第三行:替换规则 - 填写需要被替换的内容; |
||||
* 第四行:替换为 - 填写想替换成的内容(如不填则默认表示删除第三行里填写的内容); |
||||
* 第五行:替换范围,选填书名或者源名 - 填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。 |
||||
* 注:如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第三行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行百度学习)。 |
||||
|
||||
|
||||
## 备份相关 |
||||
|
||||
### 1. 云备份在哪? |
||||
“**我的**”——“**备份与恢复**”——“**WebDav 设置**”。 |
||||
|
||||
### 2. 如何操作进行云备份? |
||||
* 侧栏设置,WebDav 设置; |
||||
* 正确填写 WebDAV 服务器地址、账号和密码; |
||||
* 无需操作,APP 默认每天自动云备份一次。 |
||||
1. 云备份在哪? |
||||
* 我的 - 备份与恢复 - WebDav设置。 |
||||
|
||||
作者在此诚挚推荐使用【坚果云】进行 WebDav 备份。 |
||||
2. 如何操作进行云备份? |
||||
* 侧栏设置,WebDav设置; |
||||
* 正确填写WebDAV 服务器地址、WebDAV 账号、WebDAV 密码;(要获得这三项的信息,需要注册一个坚果云账号,如果直接在手机上注册,坚果云会让你下载app,过程比较麻烦,为了一步到位,最好是在电脑上打开这个注册链接:https://www.jianguoyun.com/d/signup ;注册后,进入坚果云;点击右上角账户名处选择 “账户信息”,然后选择“安全选项”;在“安全选项” 中找到“第三方应用管理”,并选择“添加应用”,输入名称如“阅读”后,会生成密码,选择完成;其中 https://dav.jianguoyun.com/dav/ 就是填入“WebDAV 服务器地址”的内容,“使用情况”后面的邮箱地址就是你的“WebDAV 账号”,点击显示密码后得到的密码就是你的“WebDAV 密码”。) |
||||
* 无需操作,APP默认每天自动云备份一次。 |
||||
|
||||
如果直接在手机上注册,须下载【坚果云】APP,步骤较为繁琐。推荐在电脑上进行操作: |
||||
1. 打开注册链接:https://www.jianguoyun.com/d/signup ; |
||||
2. 注册后,进入坚果云; |
||||
3. 点击右上角账户名处选择“**账户信息**”,然后选择“**安全选项**”; |
||||
4. 在“**安全选项**”中找到“**第三方应用管理**”,并选择“**添加应用**”,输入名称(如“阅读”)后,会生成密码,选择完成; |
||||
5. 其中 `https://dav.jianguoyun.com/dav/` 就是填入“**WebDAV 服务器地址**”的内容,“**使用情况**”后面的邮箱地址就是你的“**WebDAV 账号**”,点击“**显示密码**“后得到的密码就是你的“**WebDAV 密码**”。 |
||||
3. 关于云备份的相关说明 |
||||
* 在正确设置好云备份的情况下,APP默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。 |
||||
|
||||
### 3. 关于云备份的相关说明 |
||||
4. 本地备份和云备份都能备份哪些东西? |
||||
* 书架、看书进度、搜索记录、书源、替换、APP设置等都会备份,基本涵盖所有内容。 |
||||
|
||||
在正确设置好云备份的情况下,APP 默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。 |
||||
|
||||
### 4. 本地备份和云备份都能备份哪些东西? |
||||
书架、看书进度、搜索记录、书源、替换和 APP 设置等都会备份,基本涵盖所有内容。 |
||||
|
||||
### 5. 出现某些未知 Bug 怎么办? |
||||
清除软件数据试试看,不行再进行反馈。 |
||||
5. 出现某些未知bug怎么办? |
||||
* 清除软件数据试试看,不行再进行反馈。 |
||||
|
||||
|
||||
## 其他 |
||||
1. 如何听书? |
||||
* 可以使用手机自带的朗读引擎,也可使用第三方如谷歌、小米等朗读引擎。 |
||||
* 【具体操作:安装-系统设置-其他高级设置-辅助功能-TTS输出-选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。】 |
||||
|
||||
### 1. 如何听书? |
||||
可以使用手机自带的朗读引擎,也可使用第三方如 Google(谷歌)或小米等朗读引擎。 |
||||
|
||||
【具体操作】*安装——系统设置——其他高级设置——辅助功能——TTS 输出——选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。* |
||||
|
||||
### 2. 如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页或自定义翻页按键? |
||||
阅读界面——“**设置**”(可上划,下面还有其他设置)。 |
||||
2. 如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页、自定义翻页案件? |
||||
* 阅读界面,设置(可上划,下面还有其他设置)。 |
||||
|
||||
### 3. 搜索的时候感觉手机卡顿,如何解决? |
||||
“**我的**”——“**其他设置**”——调低“**更新和搜索线程数**”。 |
||||
3. 搜索的时候感觉手机卡顿,如何解决? |
||||
* 我的 - 其他设置 - “更新和搜索线程数”调低。 |
||||
|
@ -1,323 +0,0 @@ |
||||
# js变量和函数 |
||||
|
||||
书源规则中使用js可访问以下变量 |
||||
> java 变量-当前类 |
||||
> baseUrl 变量-当前url,String |
||||
> result 变量-上一步的结果 |
||||
> book 变量-[书籍类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt) |
||||
> chapter 变量-[当前目录类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookChapter.kt) |
||||
> source 变量-[基础书源类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BaseSource.kt) |
||||
> cookie 变量-[cookie操作类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/http/CookieStore.kt) |
||||
> cache 变量-[缓存操作类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/CacheManager.kt) |
||||
> title 变量-当前标题,String |
||||
> src 内容,源码 |
||||
> nextChapterUrl 变量 下一章节url |
||||
|
||||
## 当前类对象的可使用的部分方法 |
||||
|
||||
### [AnalyzeUrl](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt) 部分函数 |
||||
> js中通过java.调用,只在`登录检查JS`规则中有效 |
||||
``` |
||||
initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问 |
||||
getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头 |
||||
getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果 |
||||
getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测 |
||||
``` |
||||
|
||||
### [AnalyzeRule](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt) 部分函数 |
||||
* 获取文本/文本列表 |
||||
> `mContent` 待解析源代码,默认为当前页面 |
||||
> `isUrl` 链接标识,默认为`false` |
||||
``` |
||||
java.getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false) |
||||
java.getStringList(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false) |
||||
``` |
||||
* 设置解析内容 |
||||
|
||||
``` |
||||
java.setContent(content: Any?, baseUrl: String? = null): |
||||
``` |
||||
|
||||
* 获取Element/Element列表 |
||||
|
||||
> 如果要改变解析源代码,请先使用`java.setContent` |
||||
|
||||
``` |
||||
java.getElement(ruleStr: String) |
||||
java.getElements(ruleStr: String) |
||||
``` |
||||
|
||||
* 重新搜索书籍/重新获取目录url |
||||
|
||||
> 可以在刷新目录之前使用,有些书源书籍地址和目录url会变 |
||||
|
||||
``` |
||||
java.reGetBook() |
||||
java.refreshTocUrl() |
||||
``` |
||||
* 变量存取 |
||||
|
||||
``` |
||||
java.get(key) |
||||
java.put(key, value) |
||||
``` |
||||
|
||||
### [js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt) 部分函数 |
||||
|
||||
* 网络请求 |
||||
|
||||
``` |
||||
java.ajax(urlStr): String |
||||
java.ajaxAll(urlList: Array<String>): Array<StrResponse?> |
||||
//返回Response 方法body() code() message() header() raw() toString() |
||||
java.connect(urlStr): StrResponse |
||||
|
||||
java.post(url: String, body: String, headerMap: Map<String, String>): Connection.Response |
||||
|
||||
java.get(url: String, headerMap: Map<String, String>): Connection.Response |
||||
|
||||
java.head(url: String, headerMap: Map<String, String>): Connection.Response |
||||
|
||||
* 使用webView访问网络 |
||||
* @param html 直接用webView载入的html, 如果html为空直接访问url |
||||
* @param url html内如果有相对路径的资源不传入url访问不了 |
||||
* @param js 用来取返回值的js语句, 没有就返回整个源代码 |
||||
* @return 返回js获取的内容 |
||||
java.webView(html: String?, url: String?, js: String?): String |
||||
|
||||
* 使用内置浏览器打开链接,可用于获取验证码 手动验证网站防爬 |
||||
* @param url 要打开的链接 |
||||
* @param title 浏览器的标题 |
||||
java.startBrowser(url: String, title: String) |
||||
|
||||
* 使用内置浏览器打开链接,并等待网页结果 .body()获取网页内容 |
||||
java.startBrowserAwait(url: String, title: String): StrResponse |
||||
``` |
||||
* 调试 |
||||
``` |
||||
java.log(msg) |
||||
java.logType(var) |
||||
``` |
||||
* 获取用户输入的验证码 |
||||
``` |
||||
java.getVerificationCode(imageUrl) |
||||
``` |
||||
* 弹窗提示 |
||||
``` |
||||
java.longToast(msg: Any?) |
||||
java.toast(msg: Any?) |
||||
``` |
||||
* 从网络(由java.cacheFile实现)、本地读取JavaScript文件,导入上下文请手动`eval(String(...))` |
||||
``` |
||||
java.importScript(url) |
||||
//相对路径支持android/data/{package}/cache |
||||
java.importScript(relativePath) |
||||
java.importScript(absolutePath) |
||||
``` |
||||
* 缓存网络文件 |
||||
``` |
||||
获取 |
||||
java.cacheFile(url) |
||||
java.cacheFile(url,saveTime) |
||||
执行内容 |
||||
eval(String(java.cacheFile(url))) |
||||
删除缓存文件 |
||||
cache.delete(java.md5Encode16(url)) |
||||
``` |
||||
* 获取网络zip文件里面的数据 |
||||
``` |
||||
java.getZipStringContent(url: String, path: String) |
||||
``` |
||||
* base64 |
||||
> flags参数可省略,默认Base64.NO_WRAP,查看[flags参数说明](https://blog.csdn.net/zcmain/article/details/97051870) |
||||
``` |
||||
java.base64Decode(str: String) |
||||
java.base64Decode(str: String, charset: String) |
||||
java.base64DecodeToByteArray(str: String, flags: Int) |
||||
java.base64Encode(str: String, flags: Int) |
||||
``` |
||||
* ByteArray |
||||
``` |
||||
Str转Bytes |
||||
java.strToBytes(str: String) |
||||
java.strToBytes(str: String, charset: String) |
||||
Bytes转Str |
||||
java.bytesToStr(bytes: ByteArray) |
||||
java.bytesToStr(bytes: ByteArray, charset: String) |
||||
``` |
||||
* Hex |
||||
``` |
||||
HexString 解码为字节数组 |
||||
java.hexDecodeToByteArray(hex: String) |
||||
hexString 解码为utf8String |
||||
java.hexDecodeToString(hex: String) |
||||
utf8 编码为hexString |
||||
java.hexEncodeToString(utf8: String) |
||||
``` |
||||
* 文件 |
||||
> 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存/android/data/{package}/cache/内的文件 |
||||
``` |
||||
//文件下载,content为十六进制字符串,url用于生成文件名,返回文件路径 |
||||
downloadFile(content: String, url: String): String |
||||
//文件解压,zipPath为压缩文件路径,返回解压路径 |
||||
unzipFile(zipPath: String): String |
||||
//文件夹内所有文件读取 |
||||
getTxtInFolder(unzipPath: String): String |
||||
//读取文本文件 |
||||
readTxtFile(path: String): String |
||||
//删除文件 |
||||
deleteFile(path: String) |
||||
``` |
||||
**** |
||||
> [常见加密解密算法介绍](https://www.yijiyong.com/algorithm/encryption/01-intro.html) |
||||
|
||||
> [相关概念](https://blog.csdn.net/OrangeJack/article/details/82913804) |
||||
|
||||
> [Android支持的transformation](https://developer.android.google.cn/reference/kotlin/javax/crypto/Cipher?hl=en) |
||||
|
||||
> 其他加密方式 可在js中[调用](https://m.jb51.net/article/92138.htm)[hutool-crypto](https://www.hutool.cn/docs/#/) |
||||
|
||||
* 对称加密AES/DES/TripleDES |
||||
> AES transformation默认实现AES/ECB/PKCS5Padding |
||||
> DES transformation默认实现DES/ECB/PKCS5Padding |
||||
> TripleDES tansformation默认实现DESede/ECB/PKCS5Padding |
||||
> 内部实现为cn.hutool.crypto 解密加密接口支持ByteArray|Base64String|HexString|InputStream |
||||
> 输入参数key iv 支持ByteArray|Utf8String |
||||
> 如果key iv 为Hex Base64,且需要解码为ByteArray,自行调用java.base64DecodeToByteArray java.hexDecodeToByteArray |
||||
``` |
||||
//解密为ByteArray 字符串 |
||||
java.createSymmetricCrypto(transformation, key, iv).decrypt(data) |
||||
|
||||
java.createSymmetricCrypto(transformation, key, iv).decryptStr(data) |
||||
|
||||
//加密为ByteArray Base64字符 HEX字符 |
||||
java.createSymmetricCrypto(transformation, key, iv).encrypt(data) |
||||
|
||||
java.createSymmetricCrypto(transformation, key, iv).encryptBase64(data) |
||||
|
||||
java.createSymmetricCrypto(transformation, key, iv).encryptHex(data) |
||||
``` |
||||
* 摘要 |
||||
> MD5 SHA-1 SHA-224 SHA-256 SHA-384 SHA-512 |
||||
``` |
||||
java.digestHex(data: String, algorithm: String,): String? |
||||
|
||||
java.digestBase64Str(data: String, algorithm: String,): String? |
||||
``` |
||||
* HMac(部分算法暂不支持) |
||||
> DESMAC DESMAC/CFB8 DESedeMAC DESedeMAC/CFB8 DESedeMAC64 DESwithISO9797 HmacMD5 HmacSHA* ISO9797ALG3MAC PBEwithSHA* |
||||
``` |
||||
java.HMacHex(data: String, algorithm: String, key: String): String |
||||
|
||||
java.HMacBase64(data: String, algorithm: String, key: String): String |
||||
``` |
||||
* md5 |
||||
``` |
||||
java.md5Encode(str) |
||||
java.md5Encode16(str) |
||||
``` |
||||
|
||||
## book对象的可用属性和方法 |
||||
### 属性 |
||||
> 使用方法: 在js中或{{}}中使用book.属性的方式即可获取.如在正文内容后加上 ##{{book.name+"正文卷"+title}} 可以净化 书名+正文卷+章节名称(如 我是大明星正文卷第二章我爸是豪门总裁) 这一类的字符. |
||||
``` |
||||
bookUrl // 详情页Url(本地书源存储完整文件路径) |
||||
tocUrl // 目录页Url (toc=table of Contents) |
||||
origin // 书源URL(默认BookType.local) |
||||
originName //书源名称 or 本地书籍文件名 |
||||
name // 书籍名称(书源获取) |
||||
author // 作者名称(书源获取) |
||||
kind // 分类信息(书源获取) |
||||
customTag // 分类信息(用户修改) |
||||
coverUrl // 封面Url(书源获取) |
||||
customCoverUrl // 封面Url(用户修改) |
||||
intro // 简介内容(书源获取) |
||||
customIntro // 简介内容(用户修改) |
||||
charset // 自定义字符集名称(仅适用于本地书籍) |
||||
type // 0:text 1:audio |
||||
group // 自定义分组索引号 |
||||
latestChapterTitle // 最新章节标题 |
||||
latestChapterTime // 最新章节标题更新时间 |
||||
lastCheckTime // 最近一次更新书籍信息的时间 |
||||
lastCheckCount // 最近一次发现新章节的数量 |
||||
totalChapterNum // 书籍目录总数 |
||||
durChapterTitle // 当前章节名称 |
||||
durChapterIndex // 当前章节索引 |
||||
durChapterPos // 当前阅读的进度(首行字符的索引位置) |
||||
durChapterTime // 最近一次阅读书籍的时间(打开正文的时间) |
||||
canUpdate // 刷新书架时更新书籍信息 |
||||
order // 手动排序 |
||||
originOrder //书源排序 |
||||
variable // 自定义书籍变量信息(用于书源规则检索书籍信息) |
||||
``` |
||||
### 方法 |
||||
``` |
||||
//可在正文js中关闭净化 对于漫画源有用 |
||||
book.setUseReplaceRule(boolean) |
||||
``` |
||||
|
||||
## chapter对象的部分可用属性 |
||||
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符. |
||||
``` |
||||
url // 章节地址 |
||||
title // 章节标题 |
||||
baseUrl //用来拼接相对url |
||||
bookUrl // 书籍地址 |
||||
index // 章节序号 |
||||
resourceUrl // 音频真实URL |
||||
tag // |
||||
start // 章节起始位置 |
||||
end // 章节终止位置 |
||||
variable //变量 |
||||
``` |
||||
|
||||
## source对象的部分可用函数 |
||||
* 获取书源url |
||||
``` |
||||
source.getKey() |
||||
``` |
||||
* 书源变量存取 |
||||
``` |
||||
source.setVariable(variable: String?) |
||||
source.getVariable() |
||||
``` |
||||
|
||||
* 登录头操作 |
||||
``` |
||||
source.getLoginHeader() |
||||
source.getLoginHeaderMap().get(key: String) |
||||
source.putLoginHeader(header: String) |
||||
source.removeLoginHeader() |
||||
``` |
||||
* 用户登录信息操作 |
||||
> 使用`登录UI`规则,并成功登录,阅读自动加密保存登录UI规则中除type为button的信息 |
||||
``` |
||||
source.getLoginInfo() |
||||
source.getLoginInfoMap().get(key: String) |
||||
source.removeLoginInfo() |
||||
``` |
||||
## cookie对象的部分可用函数 |
||||
``` |
||||
获取全部cookie |
||||
cookie.getCookie(url) |
||||
获取cookie某一键值 |
||||
cookie.getKey(url,key) |
||||
删除cookie |
||||
cookie.removeCookie(url) |
||||
``` |
||||
|
||||
## cache对象的部分可用函数 |
||||
> saveTime单位:秒,可省略 |
||||
> 保存至数据库和缓存文件(50M),保存的内容较大时请使用`getFile putFile` |
||||
``` |
||||
保存 |
||||
cache.put(key, value , saveTime) |
||||
读取数据库 |
||||
cache.get(key) |
||||
删除 |
||||
cache.delete(key) |
||||
缓存文件内容 |
||||
cache.putFile(key, value, saveTime) |
||||
读取文件内容 |
||||
cache.getFile(key) |
||||
``` |
@ -1,60 +0,0 @@ |
||||
# WebDav 书籍简明使用教程 |
||||
|
||||
> 本帮助页会在第一次进入时弹出,后续则不再出现,如想查看,请点击右上角 “**⁝**” > 帮助 查看此页。 |
||||
|
||||
虽然阅读主要是用来看网络小说的工具,但为了方便书友,也提供了一些本地书籍阅读的简单支持(epub、txt) |
||||
|
||||
但阅读本地书籍的一个难题就是如何在多设备上同步阅读进度以及书籍,假如换了设备之后,原来设备上的本地书籍也要再次手动导入,不太方便。 |
||||
|
||||
阅读本身没有自己的服务器,没有类似多看、微信读书那种服务器存储的可能性,但是,阅读支持 WebDav 备份,那么我们也可以利用 WebDav 来同步书籍。 |
||||
|
||||
### 前提条件 |
||||
1. 配置好书籍存储位置(WebDav书籍下载存储到的位置):依次点击我的/其他设置/书籍存储位置,选择书籍保存位置即可。 |
||||
|
||||
2. 配置好 WebDav 备份(WebDav书籍的保存位置):我的/备份与恢复/WebDav设置。这里需要配置 WebDav 备份的服务器地址、账号、密码。详细的配置方案这里不赘述,请看这篇文章:[坚果云注册与配置 · 语雀 (yuque.com)](https://www.yuque.com/legado/wiki/fkx510) 或点击该页面右上角的帮助按钮,查看配置方法。 |
||||
|
||||
### 上传书籍到 WebDav |
||||
|
||||
配置好 WebDav 后,从主界面进入 WebDav 书籍页没有任何书籍显示,这是很正常的,因为我们WebDav的服务器上还没有任何书籍。 |
||||
|
||||
目前将书籍上传到 WebDav 的方式有三种: |
||||
|
||||
1. App 上传已导入的本地书籍。 |
||||
|
||||
长按已导入的本地书籍进入书籍详情 > 右上角 “**⁝**” 找到 **上传 WebDav** ,点击,等待几秒后即可上传成功。 |
||||
|
||||
2. App 上传已缓存的网络书籍。 |
||||
|
||||
主界面右上角点击更多设置 > 点击缓存/导出,在此页面右上角 “**⁝**” 找到 **导出到 WebDav** 并勾选。那么在书籍导出的时候便会自动上传一份到 WebDav 服务器中。 |
||||
|
||||
3. 使用坚果云客户端/自建WebDav服务客户端上传。 |
||||
|
||||
对于大部分用户来说,App上传足够了,但有些用户书籍数量可能比较大,那么我们不建议您一本一本通过 App 上传,更好的方式是使用您所使用的 WebDav 服务的客户端批量上传。 |
||||
|
||||
假设我们使用的坚果云的 WebDav 服务,进入 [坚果云官网](https://www.jianguoyun.com/d/home#/) ,下载对应平台的客户端安装运行,找到文件夹目录 legado/books ,这里就是存放书籍的位置,您可以批量将书籍上传到该文件夹下。 |
||||
|
||||
**不管是使用上述的任何一种方式上传的书籍,为了确保上传无误,请您最好在上传书籍后进入 WebDav 书籍页 检查是否能看到已经上传的书籍。** |
||||
|
||||
### 下载 WebDav 书籍到本地 |
||||
|
||||
与上传方式的多种多样不同,下载书籍到本地的方式比较朴素。 |
||||
|
||||
在 **WebDav 书籍页** 浏览已经上传的书籍,找到自己要下载的书籍,点击 **加入书架** 按钮,软件则会自动下载该书籍到本地并加入到书架中。 |
||||
|
||||
### 注意事项 |
||||
|
||||
- 如果使用的是坚果云的 WebDav 服务,免费流量限额对于同步App设置等以及 **少量的书籍** 足够了。但是如果是频繁需要上传/下载书籍的用户流量可能就不太够用了,请注意个人的用量,避免出现超出限额影响 App 设置等的同步。 |
||||
|
||||
### 常见问题 |
||||
|
||||
- 进入 **WebDav书籍页** 提示 "获取WebDav书籍出错 webDav 没有配置"。 |
||||
|
||||
> 这是因为没有配置 WebDav 同步服务,按照上文 前提条件 中提到的配置 Webdav 同步的方法配置好就行了。 |
||||
|
||||
- A 设备上传的本地书籍能否在 B 设备上看到,是否能够自动加到书架? |
||||
|
||||
> 如果 A 设备和 B 设备配置了相同的 WebDav 服务,那么 B 在 **WebDav 书籍页** 就能看到 A 上传的书籍。但是无法直接在书架上看到该书籍,这个可能后续会想方案来做,目前必须自己在 **WebDav 书籍页** 找到该书籍手动点击 **加入书架** 导入才行。 |
||||
|
||||
- 本地书籍的阅读进度/书签等是否同步? |
||||
|
||||
> 可以同步。 |
@ -1,7 +0,0 @@ |
||||
**是否同意本协议** |
||||
|
||||
* 本应用没有服务端,不收集任何用户信息,只采用了Google Firebase收集崩溃报告和性能报告. |
||||
* 本应用网络同步和备份采用webDav协议,由用户自己提供同步服务. |
||||
* 存储权限用来打开本地文件和本地备份恢复. |
||||
* 其它一些权限是Google Firebase需要. |
||||
* 本应用为开源软件,使用中发生任何问题由用户自己承担. |
@ -1,2 +0,0 @@ |
||||
* 由于安卓的存储访问限制,阅读需要设置**公共目录下的子目录**来实现书籍拷贝、下载,例如Documents/Books、Download/Books |
||||
* 如不设置,将无法正常使用本地书籍、webDav书籍的相关功能 |
@ -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,516 @@ |
||||
// 简化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: |
||||
} |
||||
}); |
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 799 B |
After Width: | Height: | Size: 1.2 KiB |