Compare commits

...

No commits in common. 'master' and 'record2021' have entirely different histories.

  1. 52
      .github/ISSUE_TEMPLATE/01-bugReport.md
  2. 53
      .github/ISSUE_TEMPLATE/01-bugReport.yml
  3. 19
      .github/ISSUE_TEMPLATE/02-featureRequest.md
  4. 32
      .github/ISSUE_TEMPLATE/02-featureRequest.yml
  5. 40
      .github/dependabot.yml
  6. 36
      .github/scripts/cronet.sh
  7. 2
      .github/workflows/autoupdatefork.yml
  8. 44
      .github/workflows/cronet.yml
  9. 146
      .github/workflows/release.yml
  10. 26
      .github/workflows/stale.yml
  11. 81
      .github/workflows/test.yml
  12. 1
      .idea/.name
  13. 4
      .idea/codeStyles/Project.xml
  14. 1
      .idea/inspectionProfiles/profiles_settings.xml
  15. 13
      CHANGELOG.md
  16. 149
      English.md
  17. 2
      LICENSE
  18. 35
      README.md
  19. 98
      api.md
  20. 164
      app/build.gradle
  21. BIN
      app/cronetlib/cronet_api.jar
  22. BIN
      app/cronetlib/cronet_impl_common_java.jar
  23. BIN
      app/cronetlib/cronet_impl_native_java.jar
  24. BIN
      app/cronetlib/cronet_impl_platform_java.jar
  25. 3
      app/download.gradle
  26. BIN
      app/lib/rhino-1.7.13-1.jar
  27. 176
      app/proguard-rules.pro
  28. 1537
      app/schemas/io.legado.app.data.AppDatabase/43.json
  29. 1557
      app/schemas/io.legado.app.data.AppDatabase/44.json
  30. 1618
      app/schemas/io.legado.app.data.AppDatabase/45.json
  31. 1618
      app/schemas/io.legado.app.data.AppDatabase/46.json
  32. 1625
      app/schemas/io.legado.app.data.AppDatabase/47.json
  33. 1633
      app/schemas/io.legado.app.data.AppDatabase/48.json
  34. 1654
      app/schemas/io.legado.app.data.AppDatabase/49.json
  35. 1666
      app/schemas/io.legado.app.data.AppDatabase/50.json
  36. 1674
      app/schemas/io.legado.app.data.AppDatabase/51.json
  37. 1680
      app/schemas/io.legado.app.data.AppDatabase/52.json
  38. 1692
      app/schemas/io.legado.app.data.AppDatabase/53.json
  39. 1711
      app/schemas/io.legado.app.data.AppDatabase/54.json
  40. 1711
      app/schemas/io.legado.app.data.AppDatabase/55.json
  41. 1711
      app/schemas/io.legado.app.data.AppDatabase/56.json
  42. 1717
      app/schemas/io.legado.app.data.AppDatabase/57.json
  43. 1724
      app/schemas/io.legado.app.data.AppDatabase/58.json
  44. 14
      app/src/app/AndroidManifest.xml
  45. 17
      app/src/app/java/io/legado/app/help/AppIntentType.kt
  46. 46
      app/src/app/java/io/legado/app/help/AppUpdateGitHub.kt
  47. 66
      app/src/app/java/io/legado/app/lib/cronet/BodyUploadProvider.kt
  48. 112
      app/src/app/java/io/legado/app/lib/cronet/CronetCoroutineInterceptor.kt
  49. 77
      app/src/app/java/io/legado/app/lib/cronet/CronetInterceptor.kt
  50. 75
      app/src/app/java/io/legado/app/lib/cronet/LargeBodyUploadProvider.kt
  51. 48
      app/src/app/java/io/legado/app/lib/cronet/NewCallBack.kt
  52. 57
      app/src/app/java/io/legado/app/lib/cronet/OldCallback.kt
  53. 114
      app/src/app/res/xml/about.xml
  54. 20
      app/src/google/AndroidManifest.xml
  55. 32
      app/src/google/assets/defaultData/rssSources.json
  56. 83
      app/src/main/AndroidManifest.xml
  57. 2
      app/src/main/assets/cronet.json
  58. 5
      app/src/main/assets/defaultData/coverRule.json
  59. 4
      app/src/main/assets/defaultData/directLinkUpload.json
  60. 2
      app/src/main/assets/defaultData/httpTTS.json
  61. 162
      app/src/main/assets/defaultData/keyboardAssists.json
  62. 2
      app/src/main/assets/defaultData/readConfig.json
  63. 2
      app/src/main/assets/defaultData/rssSources.json
  64. 8
      app/src/main/assets/defaultData/themeConfig.json
  65. 334
      app/src/main/assets/defaultData/txtTocRule.json
  66. 5
      app/src/main/assets/epub/chapter.html
  67. 2
      app/src/main/assets/epub/main.css
  68. 6
      app/src/main/assets/help/SourceMBookHelp.md
  69. 1
      app/src/main/assets/help/SourceMRssHelp.md
  70. 272
      app/src/main/assets/help/appHelp.md
  71. 0
      app/src/main/assets/help/jsExtensions.md
  72. 323
      app/src/main/assets/help/jsHelp.md
  73. 4
      app/src/main/assets/help/readMenuHelp.md
  74. 155
      app/src/main/assets/help/ruleHelp.md
  75. 60
      app/src/main/assets/help/webDavBookHelp.md
  76. 4
      app/src/main/assets/help/webDavHelp.md
  77. 7
      app/src/main/assets/privacyPolicy.md
  78. 2
      app/src/main/assets/storageHelp.md
  79. 1001
      app/src/main/assets/updateLog.md
  80. 150
      app/src/main/assets/web/bookSource/index.css
  81. 428
      app/src/main/assets/web/bookSource/index.html
  82. 516
      app/src/main/assets/web/bookSource/index.js
  83. 1
      app/src/main/assets/web/bookshelf/css/about.46bc51b1.css
  84. 1
      app/src/main/assets/web/bookshelf/css/about.b9bb4fe0.css
  85. 1
      app/src/main/assets/web/bookshelf/css/chunk-vendors.57f380c8.css
  86. 1
      app/src/main/assets/web/bookshelf/css/chunk-vendors.8a465a1d.css
  87. 1
      app/src/main/assets/web/bookshelf/css/detail.e03dc50b.css
  88. 1
      app/src/main/assets/web/bookshelf/css/detail.ed5f6dca.css
  89. BIN
      app/src/main/assets/web/bookshelf/favicon.ico
  90. BIN
      app/src/main/assets/web/bookshelf/img/error.1238cf6b.png
  91. BIN
      app/src/main/assets/web/bookshelf/img/icons/android-chrome-192x192.png
  92. BIN
      app/src/main/assets/web/bookshelf/img/icons/android-chrome-512x512.png
  93. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon-120x120.png
  94. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon-152x152.png
  95. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon-180x180.png
  96. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon-60x60.png
  97. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon-76x76.png
  98. BIN
      app/src/main/assets/web/bookshelf/img/icons/apple-touch-icon.png
  99. BIN
      app/src/main/assets/web/bookshelf/img/icons/favicon-16x16.png
  100. BIN
      app/src/main/assets/web/bookshelf/img/icons/favicon-32x32.png
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,52 @@
---
name: "[BUG] 问题提交模板"
about: 请从符号">"后面开始填写内容,填写内容可以参考 https://github.com/gedoor/legado/issues/505                    
title: "[BUG] "
labels: 'BUG'
assignees: ''
---
### 机型(如Redmi K30 Pro)
>
### 安卓版本(如Android 7.1.1)
>
### 阅读Legdao版本(我的-关于-版本,如3.20.112220)
>
### 网络环境(移动,联通,电信,移动宽带,联通宽带,电信宽带,等等..)
>
### 问题描述(简要描述发生的问题)
>
### 使用书源(填写URL或者JSON)
>
```json
```
### 复现步骤(详细描述导致问题产生的操作步骤,如果能稳定复现)
>
### 日志提交(问题截图或者日志)
>

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

@ -11,7 +11,7 @@ jobs:
if: ${{ github.event.repository.owner.id == github.event.sender.id && github.actor != 'gedoor' }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Install git

@ -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,133 +1,97 @@
name: Release Build
name: Build and Release
on:
# push:
# branches:
# - master
# paths:
# - 'CHANGELOG.md'
workflow_dispatch:
push:
branches:
- master
paths:
- 'CHANGELOG.md'
# schedule:
# - cron: '0 4 * * *'
jobs:
prepare:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
outputs:
version: ${{ steps.set-ver.outputs.version }}
play: ${{ steps.check.outputs.play }}
sign: ${{ steps.check.outputs.sign }}
steps:
- id: set-ver
run: |
echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT
- id: check
run: |
if [ ! -z "${{ secrets.RELEASE_KEY_STORE }}" ]; then
echo "sign=yes" >> $GITHUB_OUTPUT
fi
if [ ! -z "${{ secrets.SERVICE_ACCOUNT_JSON }}" ]; then
echo "play=yes" >> $GITHUB_OUTPUT
fi
build:
needs: prepare
if: ${{ needs.prepare.outputs.sign }}
strategy:
matrix:
product: [ app, google ]
fail-fast: false
runs-on: ubuntu-latest
env:
product: ${{ matrix.product }}
VERSION: ${{ needs.prepare.outputs.version }}
play: ${{ needs.prepare.outputs.play }}
# 登录蓝奏云后在控制台运行document.cookie
ylogin: ${{ secrets.LANZOU_ID }}
phpdisk_info: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_FOLDER_ID: 'b0f7pt4ja'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 1
fetch-depth: 0
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Release Apk Sign
run: |
# not use this output
# echo "KeyStore=yes" >> $GITHUB_OUTPUT
echo RELEASE_KEY_ALIAS='${{ secrets.RELEASE_KEY_ALIAS }}' >> gradle.properties
echo RELEASE_KEY_PASSWORD='${{ secrets.RELEASE_KEY_PASSWORD }}' >> gradle.properties
echo RELEASE_STORE_PASSWORD='${{ secrets.RELEASE_STORE_PASSWORD }}' >> gradle.properties
echo RELEASE_STORE_FILE='./key.jks' >> gradle.properties
echo ${{ secrets.RELEASE_KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks
sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Unify Version Name
run: |
echo "统一版本号"
sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
VERSION=$(date -d "8 hour" -u +3.%y.%m%d%H)
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
sed "/def version/c def version = \"$VERSION\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle
run: |
echo "开始进行${{ env.product }}构建"
echo "开始进行release构建"
chmod +x gradlew
./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
./gradlew assembleRelease --build-cache --parallel
- name: Organize the Files
run: |
mkdir -p ${{ github.workspace }}/apk/
cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
- name: Upload App To Artifact
uses: actions/upload-artifact@v3
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado_${{ env.product }}
name: legado apk
path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou
if: ${{ env.ylogin }}
run: |
path="$GITHUB_WORKSPACE/apk/"
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f7pt4ja"
- name: Release
if: ${{ env.product == 'app' }}
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
uses: softprops/action-gh-release@59c3b4891632ff9a897f99a91d7bc557467a3a22
with:
name: legado_app_${{ env.VERSION }}
tag_name: ${{ env.VERSION }}
name: legado_app_${{ env.RELEASE_VERSION }}
tag_name: ${{ env.RELEASE_VERSION }}
body_path: ${{ github.workspace }}/CHANGELOG.md
draft: false
prerelease: false
files: ${{ github.workspace }}/apk/legado_app_*.apk
files: ${{ github.workspace }}/apk/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare For GooglePlay
if: env.product == 'google' && env.play == 'yes'
run: |
mkdir -p ReleaseNotes
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-en-US
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-zh-CN
- name: Release To GooglePlay
if: env.product == 'google' && env.play == 'yes'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: io.legado.play
releaseFiles: ${{ github.workspace }}/apk/legado_google_*.apk
track: production
whatsNewDirectory: ${{ github.workspace }}/ReleaseNotes
- name: Push Assets To "release" Branch
if: ${{ github.actor == 'gedoor' }}
run: |
cd $GITHUB_WORKSPACE/apk/
cd $GITHUB_WORKSPACE/apk || exit 1
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b release
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release"
git add *.apk
git commit -m "${{ env.VERSION }}"
git commit -m "${{ env.RELEASE_VERSION }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin release
- name: Purge Jsdelivr Cache
if: ${{ github.actor == 'gedoor' }}
run: |
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.actor }}/release@release/)
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.repository }}@release/)
if echo $result |grep -q 'success.*true'; then
echo "jsdelivr缓存更新成功"
else

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

@ -4,12 +4,6 @@ on:
push:
branches:
- master
paths:
- '**'
- '!**/assets/**'
- '!**.md'
- '!**/ISSUE_TEMPLATE/**'
pull_request:
workflow_dispatch:
jobs:
@ -21,25 +15,25 @@ jobs:
lanzou: ${{ steps.check.outputs.lanzou }}
telegram: ${{ steps.check.outputs.telegram }}
steps:
- id: set-ver
run: |
echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT
echo "versionL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)" >> $GITHUB_OUTPUT
- id: check
run: |
if [ ${{ secrets.LANZOU_ID }} ]; then
echo "lanzou=yes" >> $GITHUB_OUTPUT
fi
if [ ${{ secrets.BOT_TOKEN }} ]; then
echo "telegram=yes" >> $GITHUB_OUTPUT
fi
- id: set-ver
run: |
echo "::set-output name=version::$(date -d "8 hour" -u +3.%y.%m%d%H)"
echo "::set-output name=versionL::$(date -d "8 hour" -u +3.%y.%m%d%H%M)"
- id: check
run: |
if [ ${{ secrets.LANZOU_ID }} ]; then
echo "::set-output name=lanzou::yes"
fi
if [ ${{ secrets.BOT_TOKEN }} ]; then
echo "::set-output name=telegram::yes"
fi
build:
needs: prepare
strategy:
matrix:
product: [ app, google ]
type: [ release, releaseA ]
product: [app, google]
type: [release, releaseA]
fail-fast: false
runs-on: ubuntu-latest
env:
@ -48,9 +42,17 @@ jobs:
VERSION: ${{ needs.prepare.outputs.version }}
VERSIONL: ${{ needs.prepare.outputs.versionL }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt
run: |
echo "清空18PlusList.txt"
@ -63,10 +65,6 @@ jobs:
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Build With Gradle
run: |
if [ ${{ env.type }} == 'release' ]; then
@ -87,14 +85,14 @@ jobs:
mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk
done
- name: Upload App To Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: legado.${{ env.product }}.${{ env.type }}
path: ${{ github.workspace }}/apk/*.apk
lanzou:
needs: [ prepare, build ]
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }}
needs: [prepare, build]
if: ${{ needs.prepare.outputs.lanzou }}
runs-on: ubuntu-latest
env:
# 登录蓝奏云后在控制台运行document.cookie
@ -103,8 +101,8 @@ jobs:
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_FOLDER_ID: '2670621'
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
path: apk/
- working-directory: apk/
@ -117,38 +115,37 @@ jobs:
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b"
test_Branch:
needs: [ prepare, build ]
needs: [prepare, build]
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' && github.actor == 'gedoor' }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
path: apk/
- working-directory: apk/
run: mv */*.apk . ;rm -rf */
- name: Push To "test" Branch
run: |
cd $GITHUB_WORKSPACE/apk/
cd $GITHUB_WORKSPACE/apk
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b test
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release"
git add *.apk
git commit -m "${{ needs.prepare.outputs.versionL }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin test
telegram:
needs: [ prepare, build ]
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.telegram == 'yes' }}
needs: [prepare, build]
if: ${{ needs.prepare.outputs.telegram }}
runs-on: ubuntu-latest
env:
CHANNEL_ID: ${{ secrets.CHANNEL_ID }}
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
path: apk/
- working-directory: apk/

@ -1 +0,0 @@
legado

@ -4,7 +4,6 @@
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
@ -118,6 +117,9 @@
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

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

@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Legado Copyright (C) 2022 gedoor
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

@ -1,9 +1,4 @@
# [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>
[![icon_android](https://github.com/gedoor/gedoor.github.io/blob/master/images/icon_android.png)](https://play.google.com/store/apps/details?id=io.legado.play.release)
<a href="https://data.newrank.cn/m/s.html?s=NykyOzI9MS5LNQ%3D%3D" target="_blank">
<img src="https://img.shields.io/badge/-微信关注“开源阅读”公众号-orange.svg" alt="#" align="right">
</a>
@ -26,7 +21,15 @@ Legado is a free and open source novel reader for Android.
>看看 [官方帮助文档](https://www.yuque.com/legado/wiki),也许里面就有你要的答案。
# Function-主要功能 [![](https://img.shields.io/badge/-Function-F5F5F5.svg)](#Function-主要功能-)
[English](English.md)
<details><summary>English</summary>
1. Online reading from a variety of sources.<br>
2. Local reading of downloaded content.<br>
3. A configurable reader with multiple viewers, reading directions and other settings. <br>
4. Categories to organize your library.<br>
5. Light and dark themes.<br>
6. Schedule updating your library for new chapters.<br>
7. read offline or to your desired cloud service
</details>
<details><summary>中文</summary>
1.自定义书源,自己设置规则,抓取网页数据,规则简单易懂,软件内有规则说明。<br>
@ -49,12 +52,12 @@ Legado is a free and open source novel reader for 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)
* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/legado@release/)
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
#### IOS-苹果
* 已停止(No release) - [Github](https://github.com/gedoor/YueDuFlutter)
* 准备中(No release) - [Github](https://github.com/gedoor/YueDuFlutter)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
@ -78,8 +81,8 @@ https://www.yuque.com/legado/wiki/community
# API [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-)
* 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
* 可通过url唤起阅读进行一键导入,url格式: legado://import/{path}?src={url}
* path类型: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig,addToBookshelf
* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版,添加到书架
* path类型: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig
* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
@ -87,14 +90,12 @@ https://www.yuque.com/legado/wiki/community
# Other-其他 [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-其他-)
##### 免责声明
https://gedoor.github.io/Disclaimer
https://gedoor.github.io/about.html
##### 阅读3.0
* [书源规则](https://alanskycn.gitee.io/teachme)
* [更新日志](/app/src/main/assets/updateLog.md)
* [帮助文档](/app/src/main/assets/help/appHelp.md)
* [web端书架](https://github.com/gedoor/legado_web_bookshelf)
* [web端源编辑](https://github.com/gedoor/legado_web_source_editor)
* [web端](https://github.com/celetor/web-yuedu3)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
@ -121,8 +122,8 @@ https://gedoor.github.io/Disclaimer
</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">
<img src="https://github.com/gedoor/gedoor.github.io/blob/master/images/%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/images/%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/images/%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/images/%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/images/%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/images/%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/-返回顶部-orange.svg" alt="#" align="right">

@ -1,30 +1,20 @@
# 阅读API
## 对于Web的配置
您需要先在设置中启用"Web 服务"。
## 使用
### Web
以下说明假设您的操作在本机进行,且开放端口为1234。
如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。
#### 插入单个书源
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
```
URL = http://127.0.0.1:1234/saveSource
Method = POST
```
#### 插入多个书源or订阅源
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
#### 插入多个书源or订阅源
```
URL = http://127.0.0.1:1234/saveBookSources
@ -32,6 +22,9 @@ URL = http://127.0.0.1:1234/saveRssSources
Method = POST
```
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 获取书源
```
@ -50,27 +43,25 @@ Method = GET
#### 删除多个书源or订阅源
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
```
URL = http://127.0.0.1:1234/deleteBookSources
URL = http://127.0.0.1:1234/deleteRssSources
Method = POST
```
#### 插入书籍
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 插入书籍
```
URL = http://127.0.0.1:1234/saveBook
Method = POST
```
#### 获取所有书籍
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
#### 获取所有书籍
```
URL = http://127.0.0.1:1234/getBookshelf
Method = GET
@ -79,7 +70,6 @@ Method = GET
获取APP内的所有书籍。
#### 获取书籍章节列表
```
URL = http://127.0.0.1:1234/getChapterList?url=xxx
Method = GET
@ -88,52 +78,36 @@ Method = GET
获取指定图书的章节列表。
#### 获取书籍内容
```
URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
Method = GET
```
获取指定图书的第`index`章节的文本内容。
#### 获取封面
```
URL = http://127.0.0.1:1234/cover?path=xxxxx
Method = GET
```
#### 保存书籍进度
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookProgress.kt)。
```
URL = http://127.0.0.1:1234/saveBookProgress
Method = POST
```
### Content Provider
* 需声明`io.legado.READ_WRITE`权限
* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
* 以下出现的`providerHost`请自行替换
#### 插入单个书源or订阅源
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
```
URL = content://providerHost/bookSource/insert
URL = content://providerHost/rssSource/insert
Method = insert
```
#### 插入多个书源or订阅源
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
#### 插入多个书源or订阅源
```
URL = content://providerHost/bookSources/insert
@ -141,10 +115,10 @@ URL = content://providerHost/rssSources/insert
Method = insert
```
#### 获取书源or订阅源
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
获取指定URL对应的书源信息。
用`Cursor.getString(0)`取出返回结果。
#### 获取书源or订阅源
```
URL = content://providerHost/bookSource/query?url=xxx
@ -152,21 +126,21 @@ URL = content://providerHost/rssSource/query?url=xxx
Method = query
```
#### 获取所有书源or订阅源
获取APP内的所有订阅源。
获取指定URL对应的书源信息。
用`Cursor.getString(0)`取出返回结果。
#### 获取所有书源or订阅源
```
URL = content://providerHost/bookSources/query
URL = content://providerHost/rssSources/query
Method = query
```
#### 删除多个书源or订阅源
获取APP内的所有书源。
用`Cursor.getString(0)`取出返回结果。
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 删除多个书源or订阅源
```
URL = content://providerHost/bookSources/delete
@ -174,48 +148,46 @@ URL = content://providerHost/rssSources/delete
Method = delete
```
#### 插入书籍
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 插入书籍
```
URL = content://providerHost/book/insert
Method = insert
```
#### 获取所有书籍
获取APP内的所有书籍。
用`Cursor.getString(0)`取出返回结果。
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
#### 获取所有书籍
```
URL = content://providerHost/books/query
Method = query
```
#### 获取书籍章节列表
获取指定图书的章节列表。
获取APP内的所有书籍。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍章节列表
```
URL = content://providerHost/book/chapter/query?url=xxx
Method = query
```
#### 获取书籍内容
获取指定图书的第`index`章节的文本内容。
获取指定图书的章节列表。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍内容
```
URL = content://providerHost/book/content/query?url=xxx&index=1
Method = query
```
获取指定图书的第`index`章节的文本内容。
用`Cursor.getString(0)`取出返回结果。
#### 获取封面
```
URL = content://providerHost/book/cover/query?path=xxxx
Method = query

@ -1,10 +1,8 @@
plugins {
id "com.android.application"
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
//id "com.google.gms.google-services"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'de.timfreiheit.resourceplaceholders'
apply from: 'download.gradle'
static def releaseTime() {
@ -13,16 +11,14 @@ static def releaseTime() {
def name = "legado"
def version = "3." + releaseTime()
def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim())
def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim())
android {
compileSdk = compile_sdk_version
buildToolsVersion = build_tool_version
namespace 'io.legado.app'
compileSdkVersion 32
buildToolsVersion '32.0.0'
kotlinOptions {
jvmTarget = "11"
}
signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
@ -32,24 +28,19 @@ android {
keyPassword RELEASE_KEY_PASSWORD
v1SigningEnabled true
v2SigningEnabled true
enableV3Signing = true
enableV4Signing = true
}
}
}
defaultConfig {
applicationId "io.legado.app"
minSdk 21
targetSdk 33
versionCode 10000 + gitCommits
minSdkVersion 21
targetSdkVersion 32
versionCode gitCommits
versionName version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
buildConfigField "String", "Cronet_Version", "\"$CronetVersion\""
buildConfigField "String", "Cronet_Main_Version", "\"$CronetMainVersion\""
javaCompileOptions {
annotationProcessorOptions {
arguments += [
@ -61,22 +52,21 @@ android {
}
}
buildFeatures {
buildConfig true
viewBinding true
}
buildTypes {
release {
buildConfigField "String", "Cronet_Version", "\"$CronetVersion\""
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.release'
minifyEnabled true
shrinkResources true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
buildConfigField "String", "Cronet_Version", "\"$CronetVersion\""
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig
}
@ -85,25 +75,23 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
android.applicationVariants.all { variant ->
variant.outputs.all {
def flavor = variant.productFlavors[0].name
outputFileName = "${name}_${flavor}_${defaultConfig.versionName}.apk"
android.applicationVariants.all { variant ->
variant.outputs.all {
def flavor = variant.productFlavors[0].name
outputFileName = "${name}_${flavor}_${defaultConfig.versionName}.apk"
}
}
}
flavorDimensions.add("mode")
flavorDimensions "mode"
productFlavors {
app {
dimension "mode"
manifestPlaceholders.put("APP_CHANNEL_VALUE", "app")
manifestPlaceholders = [APP_CHANNEL_VALUE: "app"]
}
google {
dimension "mode"
applicationId "io.legado.play"
manifestPlaceholders.put("APP_CHANNEL_VALUE", "google")
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
}
}
compileOptions {
@ -118,99 +106,94 @@ android {
// Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
lint {
checkDependencies true
}
tasks.withType(JavaCompile) {
//options.compilerArgs << "-Xlint:unchecked"
}
}
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
dependencies {
compileOnly "com.android.tools.build:gradle:$agp_version"
//noinspection GradleDependency,GradlePackageUpdate
coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.2.2')
coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5')
testImplementation('junit:junit:4.13.2')
androidTestImplementation('androidx.test:runner:1.5.2')
androidTestImplementation('androidx.test.espresso:espresso-core:3.5.1')
androidTestImplementation('androidx.test:runner:1.4.0')
androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0')
implementation('androidx.multidex:multidex:2.0.1')
//kotlin
//noinspection GradleDependency,DifferentStdlibGradleVersion
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
//Kotlin反射
//noinspection GradleDependency,DifferentStdlibGradleVersion
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
//
def coroutines_version = '1.6.4'
def coroutines_version = '1.6.0'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")
//Toolkit
implementation('com.github.android:renderscript-intrinsics-replacement-toolkit:b6363490c3')
//androidX
implementation('androidx.core:core-ktx:1.9.0')
implementation('androidx.appcompat:appcompat:1.6.0')
implementation('androidx.activity:activity-ktx:1.6.1')
implementation('androidx.fragment:fragment-ktx:1.5.5')
implementation('androidx.preference:preference-ktx:1.2.0')
implementation('androidx.constraintlayout:constraintlayout:2.1.4')
implementation('androidx.core:core-ktx:1.7.0')
implementation('androidx.appcompat:appcompat:1.4.0')
implementation('androidx.activity:activity-ktx:1.4.0')
implementation('androidx.fragment:fragment-ktx:1.4.0')
implementation('androidx.preference:preference-ktx:1.1.1')
implementation('androidx.constraintlayout:constraintlayout:2.1.2')
implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')
implementation('androidx.viewpager2:viewpager2:1.0.0')
implementation('androidx.webkit:webkit:1.6.0')
//google
implementation('com.google.android.material:material:1.8.0')
implementation('com.google.android.material:material:1.4.0')
implementation('com.google.android.flexbox:flexbox:3.0.0')
implementation('com.google.code.gson:gson:2.10.1')
implementation('com.google.code.gson:gson:2.8.9')
implementation('androidx.webkit:webkit:1.4.0')
//lifecycle
def lifecycle_version = '2.5.1'
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-service:$lifecycle_version")
implementation('com.jakewharton.timber:timber:5.0.1')
//media
implementation("androidx.media:media:1.6.0")
implementation("androidx.media:media:1.4.3")
def exoplayer_version = '2.16.1'
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayer_version")
implementation("com.google.android.exoplayer:extension-okhttp:$exoplayer_version")
//Splitties
def splitties_version = '3.0.0'
implementation("com.louiscad.splitties:splitties-appctx:$splitties_version")
implementation("com.louiscad.splitties:splitties-systemservices:$splitties_version")
implementation("com.louiscad.splitties:splitties-views:$splitties_version")
//lifecycle
def lifecycle_version = '2.4.0'
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
//room
def room_version = '2.4.0'
implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version")
kapt("androidx.room:room-compiler:$room_version")
androidTestImplementation("androidx.room:room-testing:$room_version")
testImplementation("androidx.room:room-testing:$room_version")
//liveEventBus
implementation('io.github.jeremyliao:live-event-bus-x:1.8.0')
//
implementation('org.jsoup:jsoup:1.15.3')
implementation('com.jayway.jsonpath:json-path:2.7.0')
implementation('org.jsoup:jsoup:1.14.3')
implementation('com.jayway.jsonpath:json-path:2.6.0')
implementation('cn.wanghaomiao:JsoupXpath:2.5.1')
implementation(project(path: ':epublib'))
//JS rhino
//implementation('com.github.gedoor:rhino-android:1.8')
implementation(fileTree(dir: 'lib', include: ['rhino-*.jar']))
implementation('com.github.gedoor:rhino-android:1.6')
//
implementation('com.squareup.okhttp3:okhttp:4.10.0')
appImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
implementation('com.squareup.okhttp3:okhttp:4.9.3')
implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
//Glide
def glideVersion = "4.14.2"
implementation("com.github.bumptech.glide:glide:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
//Svg
implementation("com.caverock:androidsvg-aar:1.4")
//Glide svg plugin
implementation("com.github.qoqa:glide-svg:4.0.2")
implementation('com.github.bumptech.glide:glide:4.12.0')
kapt('com.github.bumptech.glide:compiler:4.12.0')
//webServer
def nanoHttpdVersion = "2.3.1"
@ -218,13 +201,13 @@ dependencies {
implementation("org.nanohttpd:nanohttpd-websocket:$nanoHttpdVersion")
//
implementation('com.github.jenly1314:zxing-lite:2.3.0')
implementation('com.github.jenly1314:zxing-lite:2.1.1')
//
implementation('com.jaredrummler:colorpicker:1.1.0')
//apache
implementation('org.apache.commons:commons-text:1.10.0')
implementation('org.apache.commons:commons-text:1.9')
//MarkDown
def markwonVersion = "4.6.2"
@ -234,20 +217,11 @@ dependencies {
implementation("io.noties.markwon:html:$markwonVersion")
//
implementation('com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.10')
//,使
//noinspection GradleDependency,GradlePackageUpdate
implementation('cn.hutool:hutool-crypto:5.8.11')
implementation('com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.3')
//firebase, ,
//implementation platform('com.google.firebase:firebase-bom:30.0.1')
//implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
//implementation 'com.google.firebase:firebase-perf-ktx:20.0.6'
//LeakCanary,
//debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7')
//com.github.AmrDeveloper:CodeView代码编辑已集成到应用内
//com.github.AmrDeveloper:CodeView已集成到应用内
//epubLib集成到应用内
// LeakCanary
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}

Binary file not shown.

@ -16,7 +16,6 @@ static def generateMD5(final file) {
MessageDigest digest = MessageDigest.getInstance("MD5")
file.withInputStream() { is ->
byte[] buffer = new byte[1024]
//noinspection GroovyUnusedAssignment
int numRead = 0
while ((numRead = is.read(buffer)) > 0) {
digest.update(buffer, 0, numRead)
@ -37,7 +36,7 @@ task downloadJar(type: Download) {
])
dest libPath
overwrite true
onlyIfModified false
onlyIfModified true
}
/**
* Cronet的arm64-v8a so

Binary file not shown.

@ -149,17 +149,8 @@
-keep class **.analyzeRule.**{*;}
# 保持web类
-keep class **.web.**{*;}
# 数据类
#数据类
-keep class **.data.**{*;}
# hutool-core hutool-crypto
-keep class cn.hutool.core.**{*;}
-keep class cn.hutool.crypto.**{*;}
-dontwarn cn.hutool.**
# 缓存 Cookie
-keep class **.help.http.CookieStore{*;}
-keep class **.help.CacheManager{*;}
# StrResponse
-keep class **.help.http.StrResponse{*;}
-dontwarn rx.**
-dontwarn okio.**
@ -172,7 +163,6 @@
-dontwarn okhttp3.**
-dontwarn org.conscrypt.**
-dontwarn com.jeremyliao.liveeventbus.**
-dontwarn org.commonmark.ext.gfm.**
-keep class com.google.gson.** { *; }
-keep class com.ke.gson.** { *; }
@ -233,165 +223,9 @@
public static ** valueOf(java.lang.String);
}
## ExoPlayer 反射设置ua 保证该私有变量不被混淆
-keepclassmembers class com.google.android.exoplayer2.upstream.cache.CacheDataSource$Factory {
*** upstreamDataSourceFactory;
}
## ExoPlayer 如果还不能播放就取消注释这个
# -keep class com.google.android.exoplayer2.** {*;}
## 对外提供api
-keep class io.legado.app.api.ReturnData{*;}
#-------------------Cronet------------------------------------
# Contains flags that can be safely shared with Cronet, and thus would be
# appropriate for third-party apps to include.
# Keep all annotation related attributes that can affect runtime
-keepattributes RuntimeVisible*Annotations
-keepattributes AnnotationDefault
# Keep the annotations, because if we don't, the ProGuard rules that use them
# will not be respected. These classes then show up in our final dex, which we
# do not want - see crbug.com/628226.
-keep @interface org.chromium.base.annotations.AccessedByNative
-keep @interface org.chromium.base.annotations.CalledByNative
-keep @interface org.chromium.base.annotations.CalledByNativeUnchecked
-keep @interface org.chromium.base.annotations.DoNotInline
-keep @interface org.chromium.base.annotations.UsedByReflection
-keep @interface org.chromium.base.annotations.IdentifierNameString
# Android support library annotations will get converted to androidx ones
# which we want to keep.
-keep @interface androidx.annotation.Keep
-keep @androidx.annotation.Keep class *
-keepclasseswithmembers,allowaccessmodification class * {
@androidx.annotation.Keep <fields>;
}
-keepclasseswithmembers,allowaccessmodification class * {
@androidx.annotation.Keep <methods>;
}
# Keeps for class level annotations.
-keep,allowaccessmodification @org.chromium.base.annotations.UsedByReflection class ** {}
# Keeps for method level annotations.
-keepclasseswithmembers,allowaccessmodification class ** {
@org.chromium.base.annotations.AccessedByNative <fields>;
}
-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class ** {
@org.chromium.base.annotations.CalledByNative <methods>;
}
-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class ** {
@org.chromium.base.annotations.CalledByNativeUnchecked <methods>;
}
-keepclasseswithmembers,allowaccessmodification class ** {
@org.chromium.base.annotations.UsedByReflection <methods>;
}
-keepclasseswithmembers,allowaccessmodification class ** {
@org.chromium.base.annotations.UsedByReflection <fields>;
}
# Even unused methods kept due to explicit jni registration:
# https://crbug.com/688465.
-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class !org.chromium.base.library_loader.**,** {
native <methods>;
}
-keepclasseswithmembernames,includedescriptorclasses,allowaccessmodification class org.chromium.base.library_loader.** {
native <methods>;
}
# Use assumevalues block instead of assumenosideeffects block because Google3 proguard cannot parse
# assumenosideeffects blocks which overwrite return value.
-assumevalues class org.chromium.base.Log {
static boolean isDebug() return false;
}
# Never inline classes, methods, or fields with this annotation, but allow
# shrinking and obfuscation.
# Relevant to fields when they are needed to store strong references to objects
# that are held as weak references by native code.
-if @org.chromium.base.annotations.DoNotInline class * {
*** *(...);
}
-keep,allowobfuscation,allowaccessmodification class <1> {
*** <2>(...);
}
-keepclassmembers,allowobfuscation,allowaccessmodification class * {
@org.chromium.base.annotations.DoNotInline <methods>;
}
-keepclassmembers,allowobfuscation,allowaccessmodification class * {
@org.chromium.base.annotations.DoNotInline <fields>;
# Keep all of Cronet API as it's used by the Cronet module.
-keep public class org.chromium.net.* {
!private *;
*;
}
# Never merge classes horizontally or vertically with this annotation.
# Relevant to classes being used as a key in maps or sets.
-keep,allowaccessmodification,allowobfuscation,allowshrinking @org.chromium.base.annotations.DoNotClassMerge class *
# Keep all CREATOR fields within Parcelable that are kept.
-keepclassmembers class org.chromium.** implements android.os.Parcelable {
public static *** CREATOR;
}
# Don't obfuscate Parcelables as they might be marshalled outside Chrome.
# If we annotated all Parcelables that get put into Bundles other than
# for saveInstanceState (e.g. PendingIntents), then we could actually keep the
# names of just those ones. For now, we'll just keep them all.
-keepnames,allowaccessmodification class org.chromium.** implements android.os.Parcelable {}
# Keep all enum values and valueOf methods. See
# http://proguard.sourceforge.net/index.html#manual/examples.html
# for the reason for this. Also, see http://crbug.com/248037.
-keepclassmembers enum org.chromium.** {
public static **[] values();
}
# Mark members annotated with IdentifierNameString as identifier name strings
-identifiernamestring class * {
@org.chromium.base.annotations.IdentifierNameString *;
}
# -identifiernamestring doesn't keep the module impl around, we have to
# explicitly keep it.
-if @org.chromium.components.module_installer.builder.ModuleInterface interface *
-keep,allowobfuscation,allowaccessmodification class * extends <1> {
<init>();
}
# Proguard config for apps that depend on cronet_impl_native_java.jar.
# This constructor is called using the reflection from Cronet API (cronet_api.jar).
-keep class * extends org.chromium.net.CronetProvider{
public <init>(android.content.Context);
}
# Suppress unnecessary warnings.
-dontnote org.chromium.net.ProxyChangeListener$ProxyReceiver
-dontnote org.chromium.net.AndroidKeyStore
# Needs 'void setTextAppearance(int)' (API level 23).
-dontwarn org.chromium.base.ApiCompatibilityUtils
# Needs 'boolean onSearchRequested(android.view.SearchEvent)' (API level 23).
-dontwarn org.chromium.base.WindowCallbackWrapper
# Generated for chrome apk and not included into cronet.
-dontwarn org.chromium.base.multidex.ChromiumMultiDexInstaller
-dontwarn org.chromium.base.library_loader.LibraryLoader
-dontwarn org.chromium.base.SysUtils
-dontwarn org.chromium.build.NativeLibraries
# Objects of this type are passed around by native code, but the class
# is never used directly by native code. Since the class is not loaded, it does
# not need to be preserved as an entry point.
-dontnote org.chromium.net.UrlRequest$ResponseHeadersMap
# https://android.googlesource.com/platform/sdk/+/marshmallow-mr1-release/files/proguard-android.txt#54
-dontwarn android.support.**
# This class should be explicitly kept to avoid failure if
# class/merging/horizontal proguard optimization is enabled.
-keep class org.chromium.base.CollectionUtil
#-------------------Cronet------------------------------------
# Class.forName调用
-keep class io.legado.app.lib.cronet.CronetInterceptor{*;}
-keep class io.legado.app.lib.cronet.CronetLoader{*;}
-keep class io.legado.app.help.AppUpdateGitHub{*;}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@ -4,14 +4,22 @@
package="io.legado.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
android:name=".App"
@ -32,6 +40,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标1 -->
<activity
@ -44,6 +57,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标2 -->
<activity
@ -56,6 +74,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标3 -->
<activity
@ -68,6 +91,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标4 -->
<activity
@ -80,6 +108,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标5 -->
<activity
@ -92,6 +125,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 图标6 -->
<activity
@ -104,13 +142,16 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!-- 主界面 -->
<activity
android:name=".ui.main.MainActivity"
android:alwaysRetainTaskState="true"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:launchMode="singleTask" />
<!-- 阅读界面 -->
<activity
@ -198,7 +239,7 @@
android:screenOrientation="behind" />
<!-- txt目录规则管理 -->
<activity
android:name=".ui.book.toc.rule.TxtTocRuleActivity"
android:name=".ui.book.local.rule.TxtTocRuleActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- 替换规则界面 -->
@ -208,7 +249,7 @@
android:screenOrientation="behind" />
<!-- 书籍管理 -->
<activity
android:name=".ui.book.manage.BookshelfManageActivity"
android:name=".ui.book.arrange.ArrangeBookActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- 书源调试 -->
@ -238,11 +279,7 @@
android:launchMode="singleTop" />
<!-- 导入书籍 -->
<activity
android:name=".ui.book.import.local.ImportBookActivity"
android:launchMode="singleTop" />
<!-- 添加远程 -->
<activity
android:name=".ui.book.import.remote.RemoteBookActivity"
android:name=".ui.book.local.ImportBookActivity"
android:launchMode="singleTop" />
<!-- 发现界面 -->
<activity
@ -256,10 +293,6 @@
<activity
android:name=".ui.rss.favorites.RssFavoritesActivity"
android:launchMode="singleTop" />
<!-- 书签 -->
<activity
android:name=".ui.book.bookmark.AllBookmarkActivity"
android:launchMode="singleTop" />
<!-- 缓存界面 -->
<activity
android:name=".ui.book.cache.CacheActivity"
@ -267,7 +300,6 @@
<!-- WebView界面 -->
<activity
android:name=".ui.browser.WebViewActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="standard" />
<!-- 书源登录 -->
<activity
@ -323,12 +355,6 @@
<data android:scheme="yuedu" />
</intent-filter>
</activity>
<!-- 验证码输入 -->
<activity
android:name=".ui.association.VerificationCodeActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/AppTheme.Transparent" />
<!-- 打开文件 -->
<activity
android:name=".ui.association.FileAssociationActivity"
@ -352,8 +378,6 @@
<data android:mimeType="application/json" />
<!-- EPUB -->
<data android:mimeType="application/epub+zip" />
<!-- pdf -->
<data android:mimeType="application/pdf" />
</intent-filter>
<!-- Works when an app doesn't know the media type, e.g. Dropbox -->
<intent-filter>
@ -370,13 +394,14 @@
<!-- This media type is necessary, otherwise it won't match on the file extension -->
<data android:mimeType="*/*" />
<!--TXT-->
<data android:pathAdvancedPattern=".*\\.[tT][xX][tT]" />
<data android:pathPattern=".*\\.txt" />
<data android:pathPattern=".*\\.TXT" />
<!--JSON-->
<data android:pathAdvancedPattern=".*\\.[jJ][sS][oO][nN]" />
<data android:pathPattern=".*\\.json" />
<data android:pathPattern=".*\\.JSON" />
<!-- EPUB -->
<data android:pathAdvancedPattern=".*\\.[eE][pP][uU][bB]" />
<!-- pdf -->
<data android:pathAdvancedPattern=".*\\.[pP][dD][fF]" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.EPUB" />
</intent-filter>
</activity>

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

@ -2,7 +2,7 @@
{
"id": -100,
"name": "1.百度",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}",
"contentType": "audio/wav"
},
{

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

@ -26,6 +26,8 @@
"paddingLeft": 22,
"paddingRight": 22,
"paddingTop": 5,
"pageAnim": 3,
"pageAnimEInk": 3,
"paragraphIndent": "  ",
"paragraphSpacing": 6,
"showFooterLine": true,

@ -25,7 +25,7 @@
"enabled": true,
"singleUrl": true,
"sourceGroup": "legado",
"sourceIcon": "https://cdn.jsdelivr.net/gh/mgz0227/meowcloud/icon.png",
"sourceIcon": "https://Cloud.miaogongzi.site/images/icon.png",
"sourceName": "Meow云",
"sourceUrl": "https://pan.miaogongzi.net"
},

@ -1,12 +1,4 @@
[
{
"themeName": "默认",
"isNightTheme": false,
"primaryColor": "#795548",
"accentColor": "#E53935",
"backgroundColor": "#F5F5F5",
"bottomBackground": "#EEEEEE"
},
{
"themeName": "典雅蓝",
"isNightTheme": false,

@ -1,210 +1,128 @@
[
{
"id": -1,
"enable": true,
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和]))).{0,30}$",
"example": "第一章 假装第一章前面有空白但我不要",
"serialNumber": 0
},
{
"id": -2,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"example": "第一章 标准的粤语就是这样",
"serialNumber": 1
},
{
"id": -3,
"enable": false,
"name": "目录(匹配简介)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"example": "简介 老夫诸葛村夫",
"serialNumber": 2
},
{
"id": -4,
"enable": false,
"name": "目录(古典、轻小说备用)",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$",
"example": "第一章 比上面只多了回和话",
"serialNumber": 3
},
{
"id": -5,
"enable": false,
"name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$",
"example": "12",
"serialNumber": 4
},
{
"id": -6,
"enable": false,
"name": "大写数字(纯数字标题)",
"rule": "(?<=[ \\s])[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,12}[  \\t]{0,4}$",
"example": "一百七十",
"serialNumber": 5
},
{
"id": -7,
"enable": false,
"name": "数字混合(纯数字标题)",
"rule": "(?<=[ \\s])[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟\\d]{1,12}[  \\t]{0,4}$",
"example": "12\n一百七十",
"serialNumber": 6
},
{
"id": -8,
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$",
"example": "1、这个就是标题",
"serialNumber": 7
},
{
"id": -9,
"enable": true,
"name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\-].{1,30}$",
"example": "一、只有前面的数字有差别",
"serialNumber": 8
},
{
"id": -10,
"enable": false,
"name": "数字混合 分隔符 标题名称",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-]|\\d{1,5}[::,., 、_—\\-]).{0,30}$",
"example": "1、人参公鸡\n二百二十、boy next door",
"serialNumber": 9
},
{
"id": -11,
"enable": true,
"name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"example": "正文 我奶常山赵子龙",
"serialNumber": 10
},
{
"id": -12,
"enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO][.、]|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"example": "Chapter 1 MyGrandmaIsNB",
"serialNumber": 11
},
{
"id": -13,
"enable": false,
"name": "Chapter(去简介)",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$",
"example": "Chapter 1 MyGrandmaIsNB",
"serialNumber": 12
},
{
"id": -14,
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"example": "【第一章 后面的符号可以没有",
"serialNumber": 13
},
{
"id": -15,
"enable": false,
"name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"example": "『加个直角引号更专业』\n(11)我奶常山赵子聋",
"serialNumber": 14
},
{
"id": -16,
"enable": true,
"name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"example": "☆、晋江作者最喜欢的格式",
"serialNumber": 15
},
{
"id": -17,
"enable": true,
"name": "章/卷 序号 标题",
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"example": "卷五 开源盛世",
"serialNumber": 16
},
{
"id": -18,
"enable": false,
"name": "顶格标题",
"rule": "^\\S.{1,20}$",
"example": "20字以内顶格写的都是标题",
"serialNumber": 17
},
{
"id": -19,
"enable": false,
"name": "双标题(前向)",
"rule": "(?m)(?<=[ \\t ]{0,4})第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)",
"example": "第一章 真正的标题\n第一章 这个不要",
"serialNumber": 18
},
{
"id": -20,
"enable": false,
"name": "双标题(后向)",
"rule": "(?m)(?<=[ \\t ]{0,4}第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$",
"example": "第一章 这个标题不要\n第一章真正的标题",
"serialNumber": 19
},
{
"id": -21,
"enable": true,
"name": "书名 括号 序号",
"rule": "^.{1,20}[((][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$",
"example": "标题后面数字有括号(12)",
"serialNumber": 20
},
{
"id": -22,
"enable": true,
"name": "书名 序号",
"rule": "^.{1,20}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[  \t]{0,4}$",
"example": "标题后面数字没有括号124",
"serialNumber": 21
},
{
"id": -23,
"enable": false,
"name": "特定字符 标题 特定符号",
"rule": "(?<=\\={3,6}).{1,40}?(?=\\=)",
"example": "===起这种标题干什么===",
"serialNumber": 22
},
{
"id": -24,
"enable": true,
"name": "字数分割 分节阅读",
"rule": "(?<=[  \\t]{0,4})(?:.{0,15}分[页节章段]阅读[-_ ]|第\\s{0,4}[\\d零一二两三四五六七八九十百千万]{1,6}\\s{0,4}[页节]).{0,30}$",
"example": "分节|分页|分段阅读\n第一页",
"serialNumber": 23
},
{
"id": -25,
"enable": false,
"name": "通用规则",
"rule": "(?im)^.{0,6}(?:[引楔]子|正文(?!完|结)|[引序前]言|[序终]章|扉页|[上中下][部篇卷]|卷首语|后记|尾声|番外|={2,4}|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|页[、  ]|集(?![合和])|部(?![分是门落])|篇(?!张))).{0,40}$|^.{0,6}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟a-z]{1,8}[、.  ].{0,20}$",
"example": "激进规则,适配更多非常用格式",
"serialNumber": 24
},
{
"id": -100,
"enable": false,
"name": "默认分章规则",
"rule": "",
"example": "兜底规则,请勿改动此内容",
"serialNumber": 99
}
{
"id": -1,
"enable": true,
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"serialNumber": 0
},
{
"id": -2,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{
"id": -3,
"enable": false,
"name": "目录(匹配简介)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 2
},
{
"id": -4,
"enable": false,
"name": "目录(古典、轻小说备用)",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$",
"serialNumber": 3
},
{
"id": -5,
"enable": false,
"name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$",
"serialNumber": 4
},
{
"id": -6,
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$",
"serialNumber": 5
},
{
"id": -7,
"enable": true,
"name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\-].{1,30}$",
"serialNumber": 6
},
{
"id": -8,
"enable": true,
"name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"serialNumber": 7
},
{
"id": -9,
"enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO]\\.|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 8
},
{
"id": -10,
"enable": false,
"name": "Chapter(去简介)",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 9
},
{
"id": -11,
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"serialNumber": 10
},
{
"id": -12,
"enable": false,
"name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 11
},
{
"id": -13,
"enable":true,
"name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 12
},
{
"id": -14,
"enable": true,
"name": "章/卷 序号 标题",
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"serialNumber": 13
},
{
"id": -15,
"enable":false,
"name": "顶格标题",
"rule": "^\\S.{1,20}$",
"serialNumber": 14
},
{
"id": -16,
"enable":false,
"name": "双标题(前向)",
"rule": "(?m)(?<=[ \\t ]{0,4})第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)",
"serialNumber": 15
},
{
"id": -17,
"enable":false,
"name": "双标题(后向)",
"rule": "(?m)(?<=[ \\t ]{0,4}第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$",
"serialNumber": 16
},
{
"id":-18,
"enable": true,
"name": "标题 特殊符号 序号",
"rule": "^.{1,20}[((][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$",
"serialNumber": 17
}
]

@ -7,6 +7,11 @@
<link href="../Styles/main.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<!--<div class="logo">
<img alt="" class="logo" src="../Images/logo.png"/>
</div>-->
<br/>
<br/>
<h2 class="head">{title}</h2>
{content}
</body>

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

@ -22,8 +22,4 @@
* 置底所选
* 导出所选
* 校验所选
* 校验书源可批量校验书源,由于网络等原因结果仅限参考
* "校验成功"是指所选的校验项目全部通过
* 可正常识别搜索为空、发现为空、搜索(发现)目录为空、搜索(发现)正文为空、校验超时、js执行错误导致的失效,其余的原因视为网站失效
* 校验搜索优先使用书源填写的校验关键词,不存在时使用用户输入的关键词
* 校验结束后会自动筛选"失效"书源
* 校验失败的书源分组会加上"失效",选择"失效"分组即可批量操作

@ -18,3 +18,4 @@
* 置顶所选
* 置底所选
* 导出所选
* 校验失败的书源分组会加上"失效",选择"失效"分组即可批量操作

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

@ -15,10 +15,6 @@
* 界面->所有排版设置都在里面
* 设置->其它一些设置,找不到的设置去这里看看,可滚动
## 全文搜索
搜索本地缓存或者本地文件,不能搜索在线内容
书籍字数、净化规则数量、简繁转化、文章分段都会影响到搜索速度,请酌情启用
## 朗读设置界面
* 后台->进入后台朗读,可以做一些其它事
* 设置->朗读引擎设置,可以切换本地TTS和在线朗读,在线朗读可自定义

@ -2,7 +2,8 @@
* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html)
* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html)
* 辅助键盘❓中可插入URL参数模板,打开帮助,js教程,正则教程,选择文件
* [js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)
* 辅助键盘❓中可插入URL参数模板,打开帮助,选择文件
* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行
```
@@ 默认规则,直接写时可以省略@@
@ -10,75 +11,6 @@
@Json: json规则,直接写时以$.开头可省略@Json
: regex规则,不可省略,只可以用在书籍列表和目录列表
```
* 并发率
> 并发限制,单位ms,可填写两种格式
> `1000` 访问间隔1s
> `20/60000` 60s内访问次数20
* 书源类型: 文件
> 对于类似知轩藏书提供文件整合下载的网站,可以在书源详情的下载URL规则获取文件链接,支持多个链接,阅读会自动下载并导入
* CookieJar
> 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站
* 登录UI
> 不使用内置webView登录网站,需要使用`登录URL`规则实现登录逻辑,可使用`登录检查JS`检查登录结果
> 版本20221113重要更改:按钮支持调用`登录URL`规则里面的函数,必须实现`login`函数
```
规则填写示范
[
{
name: "telephone",
type: "text"
},
{
name: "password",
type: "password"
},
{
name: "注册",
type: "button",
action: "http://www.yooike.com/xiaoshuo/#/register?title=%E6%B3%A8%E5%86%8C"
},
{
name: "获取验证码",
type: "button",
action: "getVerificationCode()"
}
]
```
* 登录URL
> 可填写登录链接或者实现登录UI的登录逻辑的JavaScript
```
示范填写
function login() {
java.log("模拟登录请求");
java.log(source.getLoginInfoMap());
}
function getVerificationCode() {
java.log("登录UI按钮:获取到手机号码"+result.get("telephone"))
}
登录按钮函数获取登录信息
result.get("telephone")
login函数获取登录信息
source.getLoginInfo()
source.getLoginInfoMap().get("telephone")
source登录相关方法,可在js内通过source.调用,可以参考阿里云语音登录
login()
getHeaderMap(hasLoginHeader: Boolean = false)
getLoginHeader(): String?
getLoginHeaderMap(): Map<String, String>?
putLoginHeader(header: String)
removeLoginHeader()
setVariable(variable: String?)
getVariable(): String?
AnalyzeUrl相关函数,js中通过java.调用
initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问
getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头
getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果
getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测
```
* 发现url格式
```json
@ -97,6 +29,12 @@ getResponse(): Response //返回访问结果,网络朗读引擎采用的是这
]
```
* 获取登录后的cookie
```
java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd
java.getCookie("http://baidu.com", "userid") => 1234
```
* 请求头,支持http代理,socks4 socks5代理设置
```
socks5代理
@ -114,6 +52,19 @@ http代理
注意:这些请求头是无意义的,会被忽略掉
```
* js 变量和函数
```
java 变量-当前类
baseUrl 变量-当前url,String
result 变量-上一步的结果
book 变量-书籍类,方法见 io.legado.app.data.entities.Book
cookie 变量-cookie操作类,方法见 io.legado.app.help.http.CookieStore
cache 变量-缓存操作类,方法见 io.legado.app.help.CacheManager
chapter 变量-当前目录类,方法见 io.legado.app.data.entities.BookChapter
title 变量-当前标题,String
src 内容,源码
```
* url添加js参数,解析url时执行,可在访问url时处理url,例
```
https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"}
@ -149,7 +100,7 @@ https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"}
})()
```
* 图片链接支持修改headers
* 正文图片链接支持修改headers
```
let options = {
"headers": {"User-Agent": "xxxx","Referrer":baseUrl,"Cookie":"aaa=vbbb;"}
@ -157,7 +108,58 @@ let options = {
'<img src="'+src+","+JSON.stringify(options)+'">'
```
* 字体解析使用
## 部分js对象属性说明
上述js变量与函数中,一些js的对象属性用的频率较高,在此列举。方便写源的时候快速翻阅。
### 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 // 自定义书籍变量信息(用于书源规则检索书籍信息)
```
### chapter对象的可用属性
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符.
```
url // 章节地址
title // 章节标题
baseUrl //用来拼接相对url
bookUrl // 书籍地址
index // 章节序号
resourceUrl // 音频真实URL
tag //
start // 章节起始位置
end // 章节终止位置
variable //变量
```
### 字体解析使用
> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码
```
<js>
@ -173,12 +175,3 @@ let options = {
</js>
```
* 购买操作
> 可直接填写链接或者JavaScript,如果执行结果是网络链接将会自动打开浏览器
* 图片解密
> 适用于图片需要二次解密的情况,直接填写JavaScript,返回解密后的`ByteArray`
> 部分变量说明:java(仅支持[js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)),result为待解密图片的`ByteArray`,src为图片链接
* 封面解密
> 同图片解密 其中result为待解密封面的`inputStream`

@ -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 书籍页** 找到该书籍手动点击 **加入书架** 导入才行。
- 本地书籍的阅读进度/书签等是否同步?
> 可以同步。

@ -17,7 +17,3 @@
* 设置好备份之后每次退出App会自动进行备份
* WebDav同一天的备份会覆盖,不同日期的备份不会覆盖
### 手动恢复备份说明
* 从WebDav手动下载备份文件需要解压才能恢复

@ -1,7 +0,0 @@
**是否同意本协议**
* 本应用没有服务端,不收集任何用户信息,只采用了Google Firebase收集崩溃报告和性能报告.
* 本应用网络同步和备份采用webDav协议,由用户自己提供同步服务.
* 存储权限用来打开本地文件和本地备份恢复.
* 其它一些权限是Google Firebase需要.
* 本应用为开源软件,使用中发生任何问题由用户自己承担.

@ -1,2 +0,0 @@
* 由于安卓的存储访问限制,阅读需要设置**公共目录下的子目录**来实现书籍拷贝、下载,例如Documents/Books、Download/Books
* 如不设置,将无法正常使用本地书籍、webDav书籍的相关功能

File diff suppressed because it is too large Load Diff

@ -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,428 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>阅读3.0源编辑器_V4.0</title>
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="index.css"/>
</head>
<body>
<div class="editor">
<div class="setbox">
<div>
<a href="../index.html">←主页</a>
<b>书源</b>
</div>
<div class="rules">
<div><b>基本</b></div>
<div>
<div>源域名 :</div>
<textarea rows="1" id="bookSourceUrl" class="base" title="bookSourceUrl"
placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
</div>
<div>
<div>源类型 :</div>
<textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
placeholder="&lt;必填&gt;0:文本 1:音频"></textarea>
</div>
<div>
<div>源名称 :</div>
<textarea rows="1" id="bookSourceName" class="base" title="bookSourceName"
placeholder="&lt;必填&gt;会显示在源列表"></textarea>
</div>
<div>
<div>源分组 :</div>
<textarea rows="1" id="bookSourceGroup" class="base" title="bookSourceGroup"
placeholder="&lt;选填&gt;描述源的特征信息"></textarea>
</div>
<div>
<div>源注释 :</div>
<textarea rows="1" id="bookSourceComment" class="base" title="bookSourceComment"
placeholder="&lt;选填&gt;描述源作者和状态"></textarea>
</div>
<div>
<div>登录地址:</div>
<textarea rows="1" id="loginUrl" class="base" title="loginUrl"
placeholder="&lt;选填&gt;填写网站登录网址,仅在需要登录的源有用"></textarea>
</div>
<div>
<div>登录界面:</div>
<textarea rows="3" id="loginUi" class="base" title="loginUi"
placeholder="&lt;选填&gt;自定义登录界面"></textarea>
</div>
<div>
<div>登录检测:</div>
<textarea rows="3" id="loginCheckJs" class="base" title="loginCheckJs"
placeholder="&lt;选填&gt;登录检测js"></textarea>
</div>
<div>
<div>并发率 :</div>
<textarea rows="1" id="concurrentRate" class="base" title="concurrentRate"
placeholder="&lt;选填&gt;并发率"></textarea>
</div>
<div>
<div>请求头 :</div>
<textarea rows="3" id="header" class="base" title="header"
placeholder="&lt;选填&gt;客户端标识"></textarea>
</div>
<div>
<div>链接验证:</div>
<textarea rows="1" id="bookUrlPattern" class="base" title="bookUrlPattern"
placeholder="&lt;选填&gt;当详情页URL与源URL的域名不一致时有效,用于添加网址"></textarea>
</div>
<p></p>
<div><b>搜索</b></div>
<div>
<div>搜索地址:</div>
<textarea rows="1" id="searchUrl" class="base" title="searchUrl"
placeholder="[域名可省略]/search.php@kw={{key}}"></textarea>
</div>
<div>
<div>校验文字:</div>
<textarea rows="1" id="ruleSearch_checkKeyWord" class="ruleSearch"
title="checkKeyWord"
placeholder="校验关键字"></textarea>
</div>
<div>
<div>列表规则:</div>
<textarea rows="1" id="ruleSearch_bookList" class="ruleSearch" title="bookList"
placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书名规则:</div>
<textarea rows="1" id="ruleSearch_name" class="ruleSearch" title="name"
placeholder="选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>作者规则:</div>
<textarea rows="1" id="ruleSearch_author" class="ruleSearch" title="author"
placeholder="选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>分类规则:</div>
<textarea rows="1" id="ruleSearch_kind" class="ruleSearch" title="kind"
placeholder="选择节点分类信息 (规则结果为String)"></textarea>
</div>
<div>
<div>字数规则:</div>
<textarea rows="1" id="ruleSearch_wordCount" class="ruleSearch" title="wordCount"
placeholder="选择节点字数信息 (规则结果为String)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleSearch_lastChapter" class="ruleSearch"
title="lastChapter"
placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介规则:</div>
<textarea rows="1" id="ruleSearch_intro" class="ruleSearch" title="intro"
placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面规则:</div>
<textarea rows="1" id="ruleSearch_coverUrl" class="ruleSearch" title="coverUrl"
placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div>
<div>
<div>详情地址:</div>
<textarea rows="1" id="ruleSearch_bookUrl" class="ruleSearch" title="bookUrl"
placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
</div>
<p></p>
<div><b>发现</b></div>
<div>
<div>发现地址:</div>
<textarea rows="6" id="exploreUrl" class="base" title="exploreUrl"
placeholder="内容能显示在发现菜单&#10;每行一条发现分类(网址域名可省略),例:&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
</div>
<div>
<div>列表规则:</div>
<textarea rows="1" id="ruleExplore_bookList" class="ruleExplore" title="bookList"
placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书名规则:</div>
<textarea rows="1" id="ruleExplore_name" class="ruleExplore" title="name"
placeholder="选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>作者规则:</div>
<textarea rows="1" id="ruleExplore_author" class="ruleExplore" title="author"
placeholder="选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>分类规则:</div>
<textarea rows="1" id="ruleExplore_kind" class="ruleExplore" title="kind"
placeholder="选择节点分类信息 (规则结果为String)"></textarea>
</div>
<div>
<div>字数规则:</div>
<textarea rows="1" id="ruleExplore_wordCount" class="ruleExplore" title="wordCount"
placeholder="选择节点字数信息 (规则结果为String)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleExplore_lastChapter" class="ruleExplore"
title="lastChapter"
placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介规则:</div>
<textarea rows="1" id="ruleExplore_intro" class="ruleExplore" title="intro"
placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面规则:</div>
<textarea rows="1" id="ruleExplore_coverUrl" class="ruleExplore" title="coverUrl"
placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div>
<div>
<div>详情地址:</div>
<textarea rows="1" id="ruleExplore_bookUrl" class="ruleExplore" title="bookUrl"
placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
</div>
<p></p>
<div><b>详情</b></div>
<div>
<div>预处理 :</div>
<textarea rows="3" id="ruleBookInfo_init" class="ruleBookInfo" title="init"
placeholder="用于加速详情信息检索,只支持AllInOne规则"></textarea>
</div>
<div>
<div>书名规则:</div>
<textarea rows="1" id="ruleBookInfo_name" class="ruleBookInfo" title="name"
placeholder="选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>作者规则:</div>
<textarea rows="1" id="ruleBookInfo_author" class="ruleBookInfo" title="author"
placeholder="选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>分类规则:</div>
<textarea rows="1" id="ruleBookInfo_kind" class="ruleBookInfo" title="kind"
placeholder="选择节点分类信息 (规则结果为String)"></textarea>
</div>
<div>
<div>字数规则:</div>
<textarea rows="1" id="ruleBookInfo_wordCount" class="ruleBookInfo"
title="wordCount"
placeholder="选择节点字数信息 (规则结果为String)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleBookInfo_lastChapter" class="ruleBookInfo"
title="lastChapter"
placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介规则:</div>
<textarea rows="1" id="ruleBookInfo_intro" class="ruleBookInfo" title="intro"
placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面规则:</div>
<textarea rows="1" id="ruleBookInfo_coverUrl" class="ruleBookInfo" title="coverUrl"
placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div>
<div>
<div>目录地址:</div>
<textarea rows="1" id="ruleBookInfo_tocUrl" class="ruleBookInfo" title="tocUrl"
placeholder="选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)"></textarea>
</div>
<p></p>
<div><b>目录</b></div>
<div>
<div>列表规则:</div>
<textarea rows="3" id="ruleToc_chapterList" class="ruleToc" title="chapterList"
placeholder="选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>章节名称:</div>
<textarea rows="1" id="ruleToc_chapterName" class="ruleToc" title="chapterName"
placeholder="选择章节名称 (规则结果为String)"></textarea>
</div>
<div>
<div>章节地址:</div>
<textarea rows="1" id="ruleToc_chapterUrl" class="ruleToc" title="chapterUrl"
placeholder="选择章节链接 (规则结果为String类型的Url)"></textarea>
</div>
<div>
<div>收费标识:</div>
<textarea rows="1" id="ruleToc_isVip" class="ruleToc" title="isVip"
placeholder="章节是否为VIP章节 (规则结果为Bool)"></textarea>
</div>
<div>
<div>购买标识:</div>
<textarea rows="1" id="ruleToc_isPay" class="ruleToc" title="isPay"
placeholder="章节是否为已购买 (规则结果为Bool)"></textarea>
</div>
<div>
<div>章节信息:</div>
<textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"
placeholder="选择章节信息 (规则结果为String)"></textarea>
</div>
<div>
<div>翻页规则:</div>
<textarea rows="1" id="ruleToc_nextTocUrl" class="ruleToc" title="nextTocUrl"
placeholder="选择目录下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea>
</div>
<p></p>
<div><b>正文</b></div>
<div>
<div>脚本注入:</div>
<textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
</div>
<div>
<div>正文规则:</div>
<textarea rows="1" id="ruleContent_content" class="ruleContent" title="content"
placeholder="选择正文内容 (规则结果为String)"></textarea>
</div>
<div>
<div>翻页规则:</div>
<textarea rows="1" id="ruleContent_nextContentUrl" class="ruleContent"
title="nextContentUrl"
placeholder="选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"></textarea>
</div>
<div>
<div>资源正则:</div>
<textarea rows="1" id="ruleContent_sourceRegex" class="ruleContent"
title="sourceRegex"
placeholder="匹配资源的url特征,用于嗅探"></textarea>
</div>
<div>
<div>替换规则:</div>
<textarea rows="1" id="ruleContent_replaceRegex" class="ruleContent"
title="replaceRegex"
placeholder="多页内容合并后替换,用于正文净化"></textarea>
</div>
<div>
<div>图片样式:</div>
<textarea rows="1" id="ruleContent_imageStyle" class="ruleContent"
title="imageStyle"
placeholder="FULL:铺满 不填:默认样式"></textarea>
</div>
<p></p>
<div><b>其它规则</b></div>
<div>
<div>启用搜索:</div>
<textarea rows="1" id="enabled" class="base" title="enabled"
placeholder="启用: true 关闭: false (可选,默认true)"></textarea>
</div>
<div>
<div>启用发现:</div>
<textarea rows="1" id="enabledExplore" class="base" title="enabledExplore"
placeholder="启用: true 关闭: false (可选,默认true)"></textarea>
</div>
<div>
<div>搜索权重:</div>
<textarea rows="1" id="weight" class="base" title="weight"
placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea>
</div>
<div>
<div>排序编号:</div>
<textarea rows="1" id="customOrder" class="base" title="customOrder"
placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
</div>
<div style="display:none;">
<div>更新时间:</div>
<textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime"
placeholder="毫秒级时间戳 (自动生成) | 请勿手动填写"></textarea>
</div>
</div>
</div>
<div class="menu">
<svg class="button">
<text x="50%" y="55%">⇈推送源</text>
<rect id="push"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇊拉取源</text>
<rect id="pull"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋘编辑源</text>
<rect id="editor"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋙生成源</text>
<rect id="conver"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✗清空表单</text>
<rect id="initial"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↶撤销操作</text>
<rect id="undo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↷重做操作</text>
<rect id="redo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇏调试源</text>
<rect id="debug"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✓保存源</text>
<rect id="accept"></rect>
</svg>
</div>
<div class="outbox">
<div class="tabbox">
<div class="tabtitle">
<div name="编辑源" class="tab1 this">编辑源</div>
<div name="调试源" class="tab2">调试源</div>
<div name="源列表" class="tab3">源列表</div>
<div name="帮助信息" class="tab4">帮助信息</div>
</div>
<div class="tabbody">
<div class="tab1 this">
<textarea class="context" id="RuleJsonString"
placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
</div>
<div class="tab2">
<input type="text" class="inputbox" id="DebugKey" placeholder="输入搜索关键字,默认搜「我的」">
<textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
</div>
<div class="tab3">
<input type="text" class="inputbox" id="Filter"
placeholder="输入筛选关键词(源名称、源URL或源分组)后按回车筛选源">
<div class="titlebar">
<button id="Import">导入源文件</button>
<button id="Export">导出源文件</button>
<button id="Delete">删除选中源</button>
<button id="ClrAll">清空列表</button>
</div>
<div class="context" id="RuleList"></div>
</div>
<div class="tab4">
<div class="context link">
<a target="_blank" href="https://alanskycn.gitee.io/teachme">源制作教程</a>
<a target="_blank"
href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a>
<a target="_blank"
href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
<a target="_blank" href="https://www.w3cschool.cn/regex_rmjc">正则表达式教程</a>
<a target="_blank" href="https://regexr.com">正则表达式在线验证工具</a>
<div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义
<br>(?s) 前缀表示跨行解析
<br>(?m) 前缀表示逐行匹配
<br>(?i) 前缀表示忽略大小写
</div>
<a target="_blank" href="https://www.beta.browxy.com">代码在线运行工具</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save