Merge pull request #23 from gedoor/master

up
pull/379/head
口口吕 5 years ago committed by GitHub
commit 997a030c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 2
      app/build.gradle
  3. 3
      app/proguard-rules.pro
  4. 9
      app/src/main/AndroidManifest.xml
  5. 14
      app/src/main/assets/updateLog.md
  6. 319
      app/src/main/assets/web/index.html
  7. 177
      app/src/main/assets/web/index.js
  8. 2
      app/src/main/java/io/legado/app/App.kt
  9. 12
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  10. 2
      app/src/main/java/io/legado/app/base/BaseFragment.kt
  11. 4
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  12. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  13. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  14. 17
      app/src/main/java/io/legado/app/constant/RSSKeywords.kt
  15. 3
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  16. 3
      app/src/main/java/io/legado/app/data/entities/Book.kt
  17. 19
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  18. 13
      app/src/main/java/io/legado/app/help/BlurTransformation.kt
  19. 2
      app/src/main/java/io/legado/app/help/BookHelp.kt
  20. 30
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  21. 4
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  22. 12
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  23. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt
  24. 3
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  25. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt
  26. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt
  27. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  28. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  29. 38
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  30. 2
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  31. 3
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  32. 4
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  33. 3
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  34. 6
      app/src/main/java/io/legado/app/service/WebService.kt
  35. 8
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  36. 66
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt
  37. 54
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  38. 52
      app/src/main/java/io/legado/app/ui/book/changecover/DiffCallBack.kt
  39. 25
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  40. 16
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  41. 5
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  42. 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  43. 34
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  44. 24
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  45. 25
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  46. 62
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  47. 91
      app/src/main/java/io/legado/app/ui/book/read/config/ReadTypeDialog.kt
  48. 72
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  49. 48
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt
  50. 94
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  51. 1
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  52. 6
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
  53. 15
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  54. 10
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  55. 5
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  56. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  57. 8
      app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt
  58. 22
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  59. 4
      app/src/main/java/io/legado/app/ui/login/SourceLogin.kt
  60. 39
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  61. 8
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  62. 9
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  63. 8
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  64. 3
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  65. 9
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt
  66. 5
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  67. 3
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  68. 79
      app/src/main/java/io/legado/app/ui/widget/ArcView.kt
  69. 3
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  70. 6
      app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt
  71. 81
      app/src/main/java/io/legado/app/ui/widget/text/ScrollTextView.kt
  72. 7
      app/src/main/java/io/legado/app/utils/EventBusKt.kt
  73. 12
      app/src/main/res/drawable/ic_type_default_1.xml
  74. 15
      app/src/main/res/drawable/ic_type_default_2.xml
  75. 18
      app/src/main/res/drawable/ic_type_default_3.xml
  76. 254
      app/src/main/res/layout/activity_book_info.xml
  77. 12
      app/src/main/res/layout/dialog_change_source.xml
  78. 327
      app/src/main/res/layout/dialog_read_book_style.xml
  79. 36
      app/src/main/res/layout/dialog_read_padding.xml
  80. 45
      app/src/main/res/layout/dialog_read_type.xml
  81. 1
      app/src/main/res/layout/item_search.xml
  82. 13
      app/src/main/res/menu/change_cover.xml
  83. 8
      app/src/main/res/menu/change_source.xml
  84. 5
      app/src/main/res/menu/txt_toc_regex.xml
  85. 42
      app/src/main/res/values/array_values.xml
  86. 52
      app/src/main/res/values/arrays.xml
  87. 15
      app/src/main/res/values/attrs.xml
  88. 7
      app/src/main/res/values/strings.xml
  89. 5
      app/src/main/res/xml/pref_config_other.xml

@ -3,7 +3,7 @@
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
## 阅读3.0 ## 阅读3.0
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg 书源规则 https://celeter.github.io
## 免责声明 ## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/disclaimer.html

@ -140,7 +140,7 @@ dependencies {
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//liveEventBus //liveEventBus
implementation 'com.jeremyliao:live-event-bus-x:1.4.5' implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
// //
def coroutines_version = '1.3.3' def coroutines_version = '1.3.3'

@ -169,7 +169,9 @@
-dontwarn com.hwangjr.rxbus.** -dontwarn com.hwangjr.rxbus.**
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn com.jeremyliao.liveeventbus.**
-keep class com.jeremyliao.liveeventbus.** { *; }
-keep class retrofit2.**{*;} -keep class retrofit2.**{*;}
-keep class okhttp3.**{*;} -keep class okhttp3.**{*;}
-keep class okio.**{*;} -keep class okio.**{*;}
@ -186,6 +188,7 @@
-keep class com.gyf.barlibrary.* {*;} -keep class com.gyf.barlibrary.* {*;}
##JSOUP ##JSOUP
-keep class org.jsoup.**{*;} -keep class org.jsoup.**{*;}
-keep class **.xpath.**{*;}
-keep class org.slf4j.**{*;} -keep class org.slf4j.**{*;}
-dontwarn org.slf4j.** -dontwarn org.slf4j.**

@ -138,8 +138,10 @@
android:launchMode="singleTop"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="booksource" android:host="booksource"
android:scheme="yuedu" /> android:scheme="yuedu" />
@ -186,6 +188,8 @@
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<activity <activity
android:name=".ui.rss.read.ReadRssActivity" android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<activity <activity
android:name="io.legado.app.ui.book.local.ImportBookActivity" android:name="io.legado.app.ui.book.local.ImportBookActivity"
@ -205,6 +209,10 @@
<activity <activity
android:name="io.legado.app.ui.book.download.DownloadActivity" android:name="io.legado.app.ui.book.download.DownloadActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<activity
android:name=".ui.login.SourceLogin"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label"> android:label="@string/receiving_shared_label">
@ -219,7 +227,6 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.login.SourceLogin" />
<service android:name=".service.CheckSourceService" /> <service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />

@ -2,6 +2,20 @@
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
**2020/03/06**
* 添加隐藏标题
* 行距段距改成倍距,根据字体大小变化
* 修复翻页时右下角页数闪烁
* 修复朗读错行
* 添加底部分隔线,开关在边距设置里
**2020/03/05**
* 修复翻页动画
* 修复主题模式跟随
* 修复滚动翻页切换章节时跳动
* 适配阅读3.0的web做源
* 本地目录规则网络导入
**2020/03/04** **2020/03/04**
* 修复仿真翻页动画 * 修复仿真翻页动画
* 添加阅读记录同步,正常退出进入软件时同步阅读记录 * 添加阅读记录同步,正常退出进入软件时同步阅读记录

@ -3,218 +3,281 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>书源编辑器v3.8</title> <title>阅读3.0书源编辑器_V4.0</title>
<link rel="stylesheet" type="text/css" href="/index.css"/> <link rel="icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="index.css" />
</head> </head>
<body> <body>
<div class="editor"> <div class="editor">
<div class="setbox"> <div class="setbox">
<div class="rules"> <div class="rules">
<div><b>书源基础信息</b></div> <div><b>基本</b></div>
<div> <div>
<div>书源名称:</div> <div>源URL :</div>
<textarea rows="1" id="bookSourceName" placeholder="书源名称(bookSourceName) | 会显示在书源列表"></textarea> <textarea rows="1" id="bookSourceUrl" class="base" title="bookSourceUrl"
placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
</div> </div>
<div> <div>
<div>书源分组:</div> <div>源类型 :</div>
<textarea rows="1" id="bookSourceGroup" placeholder="书源分组(bookSourceGroup) | 描述书源的特征信息"></textarea> <textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
placeholder="&lt;必填&gt;0:文本 1:音频"></textarea>
</div> </div>
<div> <div>
<div>名:</div> <div>源名 :</div>
<textarea rows="1" id="bookSourceUrl" <textarea rows="1" id="bookSourceName" class="base" title="bookSourceName"
placeholder="书源URL(bookSourceUrl) | 通常填写网站主页(标头不可省略),例: https://www.qidian.com"></textarea> placeholder="&lt;必填&gt;会显示在书源列表"></textarea>
</div> </div>
<div> <div>
<div>登录网页:</div> <div>源分组 :</div>
<textarea rows="1" id="loginUrl" placeholder="登录URL(loginUrl) | 填写网站登录网址,仅在需要登录的书源有用"></textarea> <textarea rows="1" id="bookSourceGroup" class="base" title="bookSourceGroup"
placeholder="&lt;选填&gt;描述书源的特征信息"></textarea>
</div> </div>
<div><b>书籍发现规则</b></div>
<div> <div>
<div>发现菜单:</div> <div>登录地址:</div>
<textarea rows="5" id="ruleFindUrl" <textarea rows="1" id="loginUrl" class="base" title="loginUrl"
placeholder="发现分类菜单规则(ruleFindUrl),将显示在发现菜单&#10;每行一条发现分类(网址域名可省略):&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea> placeholder="&lt;选填&gt;填写网站登录网址,仅在需要登录的书源有用"></textarea>
</div> </div>
<div> <div>
<div>结果列表:</div> <div>链接验证:</div>
<textarea rows="1" id="ruleFindList" <textarea rows="1" id="bookUrlPattern" class="base" title="bookUrlPattern"
placeholder="发现页列表规则(ruleFindList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea> placeholder="&lt;选填&gt;当详情页URL与源URL的域名不一致时有效,用于添加网址"></textarea>
</div> </div>
<div> <div>
<div>书籍名称:</div> <div>请求头 :</div>
<textarea rows="1" id="ruleFindName" <textarea rows="3" id="header" class="base" title="header" placeholder="&lt;选填&gt;客户端标识"></textarea>
placeholder="发现页书名规则(ruleFindName) | 选择节点书名 (规则结果为String)"></textarea>
</div> </div>
<p></p>
<div><b>搜索</b></div>
<div> <div>
<div>书籍作者:</div> <div>搜索地址:</div>
<textarea rows="1" id="ruleFindAuthor" <textarea rows="1" id="searchUrl" class="base" title="searchUrl"
placeholder="发现页作者规则(ruleFindAuthor) | 选择节点作者 (规则结果为String)"></textarea> placeholder="[域名可省略]/search.php@kw={{key}}"></textarea>
</div> </div>
<div> <div>
<div>书籍分类:</div> <div>列表规则:</div>
<textarea rows="1" id="ruleFindKind" <textarea rows="3" id="ruleSearch_bookList" class="ruleSearch" title="bookList"
placeholder="发现页分类规则(ruleFindKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea> 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>
<div>最新章节:</div> <div>最新章节:</div>
<textarea rows="1" id="ruleFindLastChapter" <textarea rows="1" id="ruleSearch_lastChapter" class="ruleSearch" title="lastChapter"
placeholder="发现页最新章节规则(ruleFindLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea> placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>简介内容:</div> <div>简介规则:</div>
<textarea rows="1" id="ruleFindIntroduce" <textarea rows="1" id="ruleSearch_intro" class="ruleSearch" title="intro"
placeholder="发现页简介规则(ruleFindIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea> placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>封面链接:</div> <div>封面规则:</div>
<textarea rows="1" id="ruleFindCoverUrl" <textarea rows="1" id="ruleSearch_coverUrl" class="ruleSearch" title="coverUrl"
placeholder="发现页封面规则(ruleFindCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea> placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div> </div>
<div> <div>
<div>详情链接:</div> <div>详情地址:</div>
<textarea rows="1" id="ruleFindNoteUrl" <textarea rows="1" id="ruleSearch_bookUrl" class="ruleSearch" title="bookUrl"
placeholder="发现页详情规则(ruleFindNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea> placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
</div> </div>
<div><b>书籍搜索规则</b></div> <p></p>
<div><b>发现</b></div>
<div> <div>
<div>搜索网址:</div> <div>发现地址:</div>
<textarea rows="1" id="ruleSearchUrl" <textarea rows="6" id="exploreUrl" class="base" title="exploreUrl"
placeholder="搜索网址(ruleSearchUrl) | [域名可省略]/search.php@kw=searchKey|char=utf-8"></textarea> placeholder="内容能显示在发现菜单&#10;每行一条发现分类(网址域名可省略),例:&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
</div> </div>
<div> <div>
<div>结果验证:</div> <div>列表规则:</div>
<textarea rows="1" id="ruleBookUrlPattern" <textarea rows="1" id="ruleExplore_bookList" class="ruleExplore" title="bookList"
placeholder="搜索页URL验证(ruleBookUrlPattern) | 正则验证URL是否为详情页,成功则跳过搜索页解析"></textarea> placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div> </div>
<div> <div>
<div>结果列表:</div> <div>书名规则:</div>
<textarea rows="1" id="ruleSearchList" <textarea rows="1" id="ruleExplore_name" class="ruleExplore" title="name"
placeholder="搜索页列表规则(ruleSearchList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea> placeholder="选择节点书名 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>书籍名称:</div> <div>作者规则:</div>
<textarea rows="1" id="ruleSearchName" <textarea rows="1" id="ruleExplore_author" class="ruleExplore" title="author"
placeholder="搜索页书名规则(ruleSearchName) | 选择节点书名 (规则结果为String)"></textarea> placeholder="选择节点作者 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>书籍作者:</div> <div>分类规则:</div>
<textarea rows="1" id="ruleSearchAuthor" <textarea rows="1" id="ruleExplore_kind" class="ruleExplore" title="kind"
placeholder="搜索页作者规则(ruleSearchAuthor) | 选择节点作者 (规则结果为String)"></textarea> placeholder="选择节点分类信息 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>书籍分类:</div> <div>字数规则:</div>
<textarea rows="1" id="ruleSearchKind" <textarea rows="1" id="ruleExplore_wordCount" class="ruleExplore" title="wordCount"
placeholder="搜索页分类规则(ruleSearchKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea> placeholder="选择节点字数信息 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>最新章节:</div> <div>最新章节:</div>
<textarea rows="1" id="ruleSearchLastChapter" <textarea rows="1" id="ruleExplore_lastChapter" class="ruleExplore" title="lastChapter"
placeholder="搜索页最新章节规则(ruleSearchLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea> placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>简介内容:</div> <div>简介规则:</div>
<textarea rows="1" id="ruleSearchIntroduce" <textarea rows="1" id="ruleExplore_intro" class="ruleExplore" title="intro"
placeholder="搜索页简介规则(ruleSearchIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea> placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>封面链接:</div> <div>封面规则:</div>
<textarea rows="1" id="ruleSearchCoverUrl" <textarea rows="1" id="ruleExplore_coverUrl" class="ruleExplore" title="coverUrl"
placeholder="搜索页封面规则(ruleSearchCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea> placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div> </div>
<div> <div>
<div>详情链接:</div> <div>详情地址:</div>
<textarea rows="1" id="ruleSearchNoteUrl" <textarea rows="1" id="ruleExplore_bookUrl" class="ruleExplore" title="bookUrl"
placeholder="搜索页详情规则(ruleSearchNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea> placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
</div> </div>
<div><b>书籍详情规则</b></div> <p></p>
<div><b>详情</b></div>
<div> <div>
<div>页面处理:</div> <div>预处理 :</div>
<textarea rows="1" id="ruleBookInfoInit" <textarea rows="3" id="ruleBookInfo_init" class="ruleBookInfo" title="init"
placeholder="详情页信息预处理(ruleBookInfoInit) | 用于加速详情信息检索"></textarea> placeholder="用于加速详情信息检索,只支持AllInOne规则"></textarea>
</div> </div>
<div> <div>
<div>籍名称:</div> <div>名规则:</div>
<textarea rows="1" id="ruleBookName" <textarea rows="1" id="ruleBookInfo_name" class="ruleBookInfo" title="name"
placeholder="书名规则(ruleBookName) | 选择详情页书名 (规则结果为String)"></textarea> placeholder="选择节点书名 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>书籍作者:</div> <div>作者规则:</div>
<textarea rows="1" id="ruleBookAuthor" <textarea rows="1" id="ruleBookInfo_author" class="ruleBookInfo" title="author"
placeholder="作者规则(ruleBookAuthor) | 选择详情页作者 (规则结果为String)"></textarea> placeholder="选择节点作者 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>书籍分类:</div> <div>分类规则:</div>
<textarea rows="1" id="ruleBookKind" <textarea rows="1" id="ruleBookInfo_kind" class="ruleBookInfo" title="kind"
placeholder="分类规则(ruleBookKind) | 选择详情页分类信息 (规则结果为List&lt;String&gt;)"></textarea> placeholder="选择节点分类信息 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>最新章节:</div> <div>字数规则:</div>
<textarea rows="1" id="ruleBookLastChapter" <textarea rows="1" id="ruleBookInfo_wordCount" class="ruleBookInfo" title="wordCount"
placeholder="最新章节规则(ruleBookLastChapter) | 选择详情页最新章节 (规则结果为String)"></textarea> placeholder="选择节点字数信息 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>简介内容:</div> <div>最新章节:</div>
<textarea rows="1" id="ruleIntroduce" <textarea rows="1" id="ruleBookInfo_lastChapter" class="ruleBookInfo" title="lastChapter"
placeholder="简介规则(ruleIntroduce) | 选择详情页书籍简介 (规则结果为String)"></textarea> placeholder="选择节点最新章节 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>封面链接:</div> <div>简介规则:</div>
<textarea rows="1" id="ruleCoverUrl" placeholder="封面规则(ruleCoverUrl) | 选择详情页书籍封面 (规则结果为Url)"></textarea> <textarea rows="1" id="ruleBookInfo_intro" class="ruleBookInfo" title="intro"
placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>目录链接:</div> <div>封面规则:</div>
<textarea rows="1" id="ruleChapterUrl" <textarea rows="1" id="ruleBookInfo_coverUrl" class="ruleBookInfo" title="coverUrl"
placeholder="目录URL规则(ruleChapterUrl) | 选择目录页网址 (规则结果为Url, 与详情页相同时可省略)"></textarea> placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
</div> </div>
<div><b>目录列表规则</b></div>
<div> <div>
<div>目录翻页:</div> <div>目录地址:</div>
<textarea rows="1" id="ruleChapterUrlNext" <textarea rows="1" id="ruleBookInfo_tocUrl" class="ruleBookInfo" title="tocUrl"
placeholder="目录下一页规则(ruleChapterUrlNext) | 选择目录下一页链接 (规则结果为List&lt;Url&gt;)"></textarea> placeholder="选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)"></textarea>
</div> </div>
<p></p>
<div><b>目录</b></div>
<div> <div>
<div>目录列表:</div> <div>列表规则:</div>
<textarea rows="1" id="ruleChapterList" <textarea rows="3" id="ruleToc_chapterList" class="ruleToc" title="chapterList"
placeholder="目录列表规则(ruleChapterList) | 选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea> placeholder="选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div> </div>
<div> <div>
<div>章节名称:</div> <div>章节名称:</div>
<textarea rows="1" id="ruleChapterName" <textarea rows="1" id="ruleToc_chapterName" class="ruleToc" title="chapterName"
placeholder="章节名称规则(ruleChapterName) | 选择章节名称 (规则结果为String)"></textarea> placeholder="选择章节名称 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>章节链接:</div> <div>章节地址:</div>
<textarea rows="1" id="ruleContentUrl" <textarea rows="1" id="ruleToc_chapterUrl" class="ruleToc" title="chapterUrl"
placeholder="章节URL规则(ruleContentUrl) | 选择章节链接 (规则结果为Url)"></textarea> placeholder="选择章节链接 (规则结果为String类型的Url)"></textarea>
</div> </div>
<div><b>正文阅读规则</b></div>
<div> <div>
<div>章节正文:</div> <div>收费标识:</div>
<textarea rows="1" id="ruleBookContent" <textarea rows="1" id="ruleToc_isVip" class="ruleToc" title="isVip"
placeholder="正文规则(ruleBookContent) | 选择正文内容 (规则结果为String)"></textarea> placeholder="章节是否为VIP章节 (规则结果为Bool)"></textarea>
</div> </div>
<div> <div>
<div>正文翻页:</div> <div>章节信息:</div>
<textarea rows="1" id="ruleContentUrlNext" <textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"
placeholder="正文翻页URL规则(ruleContentUrlNext) | 选择下一分页(不是下一章)链接 (规则结果为Url)"></textarea> placeholder="选择章节信息 (规则结果为String)"></textarea>
</div> </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="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="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
placeholder="注入javascript,用于模拟鼠标点击等,无返回结果"></textarea>
</div>
<div>
<div>资源正则:</div>
<textarea rows="1" id="ruleContent_sourceRegex" class="ruleContent" title="sourceRegex"
placeholder="匹配资源的url特征,用于嗅探"></textarea>
</div>
<p></p>
<div><b>其它规则</b></div> <div><b>其它规则</b></div>
<div> <div>
<div>浏览标识:</div> <div>启用搜索:</div>
<textarea rows="1" id="httpUserAgent" <textarea rows="1" id="enabled" class="base" title="enabled"
placeholder="浏览器UA(HttpUserAgent) | 浏览器标识:User-Agent (可选)"></textarea> placeholder="启用: true 关闭: false (可选,默认true)"></textarea>
</div> </div>
<div> <div>
<div>排序编号:</div> <div>启用发现:</div>
<textarea rows="1" id="serialNumber" placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea> <textarea rows="1" id="enabledExplore" class="base" title="enabledExplore"
placeholder="启用: true 关闭: false (可选,默认true)"></textarea>
</div> </div>
<div> <div>
<div>搜索权重:</div> <div>搜索权重:</div>
<textarea rows="1" id="weight" placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea> <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>
<div> <div>
<div>是否启用:</div> <div>更新时间:</div>
<textarea rows="1" id="enable" placeholder="默认启用=true,手动启用=false (可选,默认true)"></textarea> <textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime"
placeholder="整数: 0~N (可选,默认0) | 暂未使用"></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -266,13 +329,15 @@
</div> </div>
<div class="tabbody"> <div class="tabbody">
<div class="tab1 this"> <div class="tab1 this">
<textarea class="context" id="RuleJsonString" placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea> <textarea class="context" id="RuleJsonString"
placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
</div> </div>
<div class="tab2"> <div class="tab2">
<input type="text" class="inputbox" id="DebugKey" placeholder="我的"> <input type="text" class="inputbox" id="DebugKey" placeholder="输入搜索关键字,默认搜「我的">
<textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea> <textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
</div> </div>
<div class="tab3"> <div class="tab3">
<input type="text" class="inputbox" id="Filter" placeholder="输入筛选关键词(源名称、源URL或源分组)后按回车筛选源">
<div class="titlebar"> <div class="titlebar">
<button id="Import">导入书源文件</button> <button id="Import">导入书源文件</button>
<button id="Export">导出书源文件</button> <button id="Export">导出书源文件</button>
@ -283,7 +348,7 @@
</div> </div>
<div class="tab4"> <div class="tab4">
<div class="context link"> <div class="context link">
<a target="_blank" href="https://gedoor.github.io/MyBookshelf/sourcerule.html">官方书源教程</a> <a target="_blank" href="https://celeter.github.io">制作教程</a>
<a target="_blank" href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</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://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
<a target="_blank" href="https://www.w3cschool.cn/regex_rmjc/?">正则表达式教程</a> <a target="_blank" href="https://www.w3cschool.cn/regex_rmjc/?">正则表达式教程</a>
@ -294,14 +359,14 @@
<br>(?i) 前缀表示忽略大小写 <br>(?i) 前缀表示忽略大小写
</div> </div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a> <a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a>
<a target="_blank" href="/bookshelf.html">阅读书架(测试)</a> <a target="_blank" href="bookshelf.html">阅读书架(测试)</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="/index.js"></script>
</body> </body>
</html> </html>

@ -20,11 +20,40 @@ function hashParam(key, val) {
// 创建书源规则容器对象 // 创建书源规则容器对象
const RuleJSON = (() => { const RuleJSON = (() => {
let ruleJson = {}; let ruleJson = {};
$$('.rules textarea').forEach(item => ruleJson[item.id] = ''); let searchJson = {};
// for (let item of $$('.rules textarea')) ruleJson[item.id] = ''; let exploreJson = {};
ruleJson.serialNumber = 0; let bookInfoJson = {};
let tocJson = {};
let contentJson = {};
// 基本以及其他
$$('.rules .base').forEach(item => ruleJson[item.title] = '');
ruleJson.lastUpdateTime = 0;
ruleJson.customOrder = 0;
ruleJson.weight = 0; ruleJson.weight = 0;
ruleJson.enable = true; ruleJson.enabled = true;
ruleJson.enabledExplore = true;
// 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
ruleJson.ruleSearch = JSON.stringify(searchJson);
// 发现规则
$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
ruleJson.ruleExplore = JSON.stringify(exploreJson);
// 详情页规则
$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
// 目录规则
$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
ruleJson.ruleToc = JSON.stringify(tocJson);
// 正文规则
$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
ruleJson.ruleContent = JSON.stringify(contentJson);
return ruleJson; return ruleJson;
})(); })();
// 选项卡Tab切换事件处理 // 选项卡Tab切换事件处理
@ -72,15 +101,113 @@ function HttpPost(url, data) {
} }
// 将书源表单转化为书源对象 // 将书源表单转化为书源对象
function rule2json() { function rule2json() {
Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value); // 转换base
RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber); Object.keys(RuleJSON).forEach(key => {
if (!key.startsWith("rule")) {
RuleJSON[key] = $('#' + key).value;
}
});
// 转换搜索规则
let searchJson = {};
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
searchJson[key] = $('#' + 'ruleSearch_' + key).value;
});
RuleJSON.ruleSearch = JSON.stringify(searchJson);
// 转换发现规则
let exploreJson = {};
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
});
RuleJSON.ruleExplore = JSON.stringify(exploreJson);
// 转换详情页规则
let bookInfoJson = {};
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
});
RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
// 转换目录规则
let tocJson = {};
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
tocJson[key] = $('#' + 'ruleToc_' + key).value;
});
RuleJSON.ruleToc = JSON.stringify(tocJson);
// 转换正文规则
let contentJson = {};
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
contentJson[key] = $('#' + 'ruleContent_' + key).value;
});
RuleJSON.ruleContent = JSON.stringify(contentJson);
RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime);
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true'; RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.weight);
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; return RuleJSON;
} }
// 将书源对象填充到书源表单 // 将书源对象填充到书源表单
function json2rule(RuleEditor) { function json2rule(RuleEditor) {
Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : ''); // 转换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 = JSON.parse(RuleEditor.ruleSearch);
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
});
}
// 转换发现规则
if (RuleEditor.ruleExplore) {
let exploreJson = JSON.parse(RuleEditor.ruleExplore);
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
});
}
// 转换详情页规则
if (RuleEditor.ruleBookInfo) {
let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
});
}
// 转换目录规则
if (RuleEditor.ruleToc) {
let tocJson = JSON.parse(RuleEditor.ruleToc);
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
});
}
// 转换正文规则
if (RuleEditor.ruleContent) {
let contentJson = JSON.parse(RuleEditor.ruleContent);
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
});
}
} }
// 记录操作过程 // 记录操作过程
var course = { "old": [], "now": {}, "new": [] }; var course = { "old": [], "now": {}, "new": [] };
@ -153,16 +280,16 @@ $('.menu').addEventListener('click', e => {
}); });
failMsg = '\n推送失败的书源将用红色字体标注!'; failMsg = '\n推送失败的书源将用红色字体标注!';
} }
alert(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length}\n成功: ${okData.length}\n失败: ${RuleSources.length - okData.length}${failMsg}`); alert(`批量推送书源到「阅读3.0APP」\n共计: ${RuleSources.length}\n成功: ${okData.length}\n失败: ${RuleSources.length - okData.length}${failMsg}`);
} }
else { else {
alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length}`); alert(`批量推送书源到「阅读3.0APP」成功!\n共计: ${RuleSources.length}`);
} }
} }
else { else {
alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`); alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`);
} }
}).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); }); }).catch(err => { alert(`批量推送书源失败,无法连接到「阅读3.0APP」!\n${err}`); });
thisNode.setAttribute('class', ''); thisNode.setAttribute('class', '');
})(); })();
return; return;
@ -181,7 +308,7 @@ $('.menu').addEventListener('click', e => {
else { else {
alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`); alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`);
} }
}).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\n${err}`); }); }).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读3.0APP」!\n${err}`); });
thisNode.setAttribute('class', ''); thisNode.setAttribute('class', '');
})(); })();
return; return;
@ -244,9 +371,9 @@ $('.menu').addEventListener('click', e => {
(async () => { (async () => {
let saveRule = [rule2json()]; let saveRule = [rule2json()];
await HttpPost(`/saveSources`, saveRule).then(json => { await HttpPost(`/saveSources`, saveRule).then(json => {
alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`); alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读3.0APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
setRule(saveRule[0]); setRule(saveRule[0]);
}).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\n${err}`); }); }).catch(err => { alert(`保存书源失败,无法连接到「阅读3.0APP」!\n${err}`); });
thisNode.setAttribute('class', ''); thisNode.setAttribute('class', '');
})(); })();
return; return;
@ -261,6 +388,26 @@ $('#DebugKey').addEventListener('keydown', e => {
$('#debug').dispatchEvent(clickEvent); $('#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 => { $('#RuleList').addEventListener('click', e => {
@ -348,7 +495,7 @@ $('.tab3>.titlebar').addEventListener('click', e => {
console.log(deleteSources); console.log(deleteSources);
console.log(`以上书源已删除!`) console.log(`以上书源已删除!`)
} }
}).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); }); }).catch(err => { alert(`删除书源失败,无法连接到「阅读3.0APP」!\n${err}`); });
} }
break; break;
case 'ClrAll': case 'ClrAll':

@ -50,7 +50,7 @@ class App : Application() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId()
applyDayNight() applyDayNight()
LiveEventBus.get() LiveEventBus
.config() .config()
.supportBroadcast(this) .supportBroadcast(this)
.lifecycleObserverAlwaysActive(true) .lifecycleObserverAlwaysActive(true)

@ -1,6 +1,7 @@
package io.legado.app.base package io.legado.app.base
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -18,6 +19,14 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
job = Job() job = Job()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
@ -30,4 +39,7 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
): Coroutine<T> { ): Coroutine<T> {
return Coroutine.async(scope, context) { block() } return Coroutine.async(scope, context) { block() }
} }
open fun observeLiveBus() {
}
} }

@ -1,5 +1,6 @@
package io.legado.app.base package io.legado.app.base
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.SupportMenuInflater
@ -18,6 +19,7 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
private set private set
val menuInflater: MenuInflater val menuInflater: MenuInflater
@SuppressLint("RestrictedApi")
get() = SupportMenuInflater(requireContext()) get() = SupportMenuInflater(requireContext())
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext

@ -2,7 +2,9 @@ package io.legado.app.constant
import java.util.regex.Pattern import java.util.regex.Pattern
object Pattern { object AppPattern {
val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
val authorRegex = "\\s*者\\s*[::]".toRegex()
} }

@ -20,6 +20,5 @@ object EventBus {
const val SHOW_RSS = "showRss" const val SHOW_RSS = "showRss"
const val WEB_SERVICE_STOP = "webServiceStop" const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload" const val UP_DOWNLOAD = "upDownload"
const val UP_TABS = "upTabs"
const val SAVE_CONTENT = "saveContent" const val SAVE_CONTENT = "saveContent"
} }

@ -27,6 +27,7 @@ object PreferKey {
const val fontFolder = "fontFolder" const val fontFolder = "fontFolder"
const val backupPath = "backupUri" const val backupPath = "backupUri"
const val threadCount = "threadCount" const val threadCount = "threadCount"
const val webPort = "webPort"
const val keepLight = "keep_light" const val keepLight = "keep_light"
const val webService = "webService" const val webService = "webService"
const val webDavUrl = "web_dav_url" const val webDavUrl = "web_dav_url"

@ -1,17 +0,0 @@
package io.legado.app.constant
object RSSKeywords {
const val RSS_ITEM = "item"
const val RSS_ITEM_TITLE = "title"
const val RSS_ITEM_LINK = "link"
const val RSS_ITEM_CATEGORY = "category"
const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
const val RSS_ITEM_ENCLOSURE = "enclosure"
const val RSS_ITEM_DESCRIPTION = "description"
const val RSS_ITEM_CONTENT = "content:encoded"
const val RSS_ITEM_PUB_DATE = "pubDate"
const val RSS_ITEM_TIME = "time"
const val RSS_ITEM_URL = "url"
const val RSS_ITEM_TYPE = "type"
}

@ -16,6 +16,9 @@ interface TxtTocRuleDao {
@get:Query("select * from txtTocRules where enable = 1 order by serialNumber") @get:Query("select * from txtTocRules where enable = 1 order by serialNumber")
val enabled: List<TxtTocRule> val enabled: List<TxtTocRule>
@get:Query("select ifNull(max(serialNumber), 0) from txtTocRules")
val lastOrderNum: Int
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rule: TxtTocRule) fun insert(vararg rule: TxtTocRule)

@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
@ -86,7 +87,7 @@ data class Book(
@IgnoredOnParcel @IgnoredOnParcel
override var tocHtml: String? = null override var tocHtml: String? = null
fun getRealAuthor() = author.replace("作者:", "") fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)

@ -1,11 +1,14 @@
package io.legado.app.data.entities package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import android.text.TextUtils
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
@Parcelize @Parcelize
@Entity( @Entity(
@ -37,4 +40,20 @@ data class ReplaceRule(
override fun hashCode(): Int { override fun hashCode(): Int {
return id.hashCode() return id.hashCode()
} }
fun isValid(): Boolean{
if (TextUtils.isEmpty(pattern)){
return false;
}
//判断正则表达式是否正确
if (isRegex){
try {
Pattern.compile(pattern);
}
catch (ex: PatternSyntaxException){
return false;
}
}
return true;
}
} }

@ -8,10 +8,8 @@ import android.renderscript.Allocation
import android.renderscript.Element import android.renderscript.Element
import android.renderscript.RenderScript import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur import android.renderscript.ScriptIntrinsicBlur
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.CenterCrop
import java.security.MessageDigest import java.security.MessageDigest
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -21,15 +19,16 @@ import kotlin.math.roundToInt
* 模糊 * 模糊
* @radius: 0..25 * @radius: 0..25
*/ */
class BlurTransformation(context: Context, private val radius: Int) : BitmapTransformation() { class BlurTransformation(context: Context, private val radius: Int) : CenterCrop() {
private val rs: RenderScript = RenderScript.create(context) private val rs: RenderScript = RenderScript.create(context)
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val transform = super.transform(pool, toTransform, outWidth, outHeight)
//图片缩小1/2 //图片缩小1/2
val width = (min(outWidth, toTransform.width) / 2f).roundToInt() val width = (min(outWidth, transform.width) / 2f).roundToInt()
val height = (min(outHeight, toTransform.height) / 2f).roundToInt() val height = (min(outHeight, transform.height) / 2f).roundToInt()
val blurredBitmap = Bitmap.createScaledBitmap(toTransform, width, height, false); val blurredBitmap = Bitmap.createScaledBitmap(transform, width, height, false);
// Allocate memory for Renderscript to work with // Allocate memory for Renderscript to work with
//分配用于渲染脚本的内存 //分配用于渲染脚本的内存
val input = Allocation.createFromBitmap( val input = Allocation.createFromBitmap(

@ -180,7 +180,7 @@ object BookHelp {
fun formatAuthor(author: String?): String { fun formatAuthor(author: String?): String {
return author return author
?.replace("\\s*者[\\s::]*".toRegex(), "") ?.replace("\\s*者\\s*[::]\n*".toRegex(), "")
?.replace("\\s+".toRegex(), " ") ?.replace("\\s+".toRegex(), " ")
?.trim { it <= ' ' } ?.trim { it <= ' ' }
?: "" ?: ""

@ -165,10 +165,21 @@ object ReadBookConfig {
if (shareLayout) shareConfig.paragraphSpacing = value if (shareLayout) shareConfig.paragraphSpacing = value
else durConfig.paragraphSpacing = value else durConfig.paragraphSpacing = value
var titleCenter: Boolean var titleMode: Int
get() = if (shareLayout) shareConfig.titleCenter else durConfig.titleCenter get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode
set(value) = set(value) =
if (shareLayout) shareConfig.titleCenter = value else durConfig.titleCenter = value if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value
var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing
set(value) =
if (shareLayout) shareConfig.titleTopSpacing = value
else durConfig.titleTopSpacing = value
var titleBottomSpacing: Int
get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing
set(value) =
if (shareLayout) shareConfig.titleBottomSpacing = value
else durConfig.titleBottomSpacing = value
var paddingBottom: Int var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom
@ -238,6 +249,12 @@ object ReadBookConfig {
if (shareLayout) shareConfig.footerPaddingTop = value if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value else durConfig.footerPaddingTop = value
var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) =
if (shareLayout) shareConfig.showFooterLine = value
else durConfig.showFooterLine = value
@Keep @Keep
class Config( class Config(
private var bgStr: String = "#EEEEEE",//白天背景 private var bgStr: String = "#EEEEEE",//白天背景
@ -253,7 +270,9 @@ object ReadBookConfig {
var letterSpacing: Float = 0.5f,//字间距 var letterSpacing: Float = 0.5f,//字间距
var lineSpacingExtra: Int = 12,//行间距 var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距 var paragraphSpacing: Int = 12,//段距
var titleCenter: Boolean = true,//标题居中 var titleMode: Int = 0,//标题居中
var titleTopSpacing: Int = 0,
var titleBottomSpacing: Int = 0,
var paddingBottom: Int = 6, var paddingBottom: Int = 6,
var paddingLeft: Int = 16, var paddingLeft: Int = 16,
var paddingRight: Int = 16, var paddingRight: Int = 16,
@ -265,7 +284,8 @@ object ReadBookConfig {
var footerPaddingBottom: Int = 6, var footerPaddingBottom: Int = 6,
var footerPaddingLeft: Int = 16, var footerPaddingLeft: Int = 16,
var footerPaddingRight: Int = 16, var footerPaddingRight: Int = 16,
var footerPaddingTop: Int = 6 var footerPaddingTop: Int = 6,
var showFooterLine: Boolean = true
) { ) {
fun setBg(bgType: Int, bg: String) { fun setBg(bgType: Int, bg: String) {
if (AppConfig.isNightTheme) { if (AppConfig.isNightTheme) {

@ -136,15 +136,19 @@ object ImportOldData {
return bookSources.size return bookSources.size
} }
fun importOldReplaceRule(json: String): Int { fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>() val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let {
if (it.isValid()){
replaceRules.add(it) replaceRules.add(it)
} }
} }
}
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) App.db.replaceRuleDao().insert(*replaceRules.toTypedArray())
return replaceRules.size return replaceRules.size
} }

@ -2,6 +2,7 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
@ -9,8 +10,10 @@ import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext import com.jayway.jsonpath.ParseContext
import io.legado.app.App import io.legado.app.App
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
@ -128,11 +131,18 @@ object Restore {
ReadBook.loadContent() ReadBook.loadContent()
} }
withContext(Main) { withContext(Main) {
if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight() App.INSTANCE.applyDayNight()
if (!BuildConfig.DEBUG) } else if (!AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight()
} else {
postEvent(EventBus.RECREATE, "true")
}
if (!BuildConfig.DEBUG) {
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
} }
} }
}
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? { private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
try { try {

@ -1,12 +1,14 @@
package io.legado.app.model.analyzeRule package io.legado.app.model.analyzeRule
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.Keep
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.ReadContext import com.jayway.jsonpath.ReadContext
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@Keep
class AnalyzeByJSonPath { class AnalyzeByJSonPath {
private var ctx: ReadContext? = null private var ctx: ReadContext? = null

@ -2,6 +2,7 @@ package io.legado.app.model.analyzeRule
import android.text.TextUtils.isEmpty import android.text.TextUtils.isEmpty
import android.text.TextUtils.join import android.text.TextUtils.join
import androidx.annotation.Keep
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -15,7 +16,7 @@ import java.util.*
* Created by GKF on 2018/1/25. * Created by GKF on 2018/1/25.
* 书源规则解析 * 书源规则解析
*/ */
@Keep
class AnalyzeByJSoup { class AnalyzeByJSoup {
private var element: Element? = null private var element: Element? = null

@ -1,8 +1,10 @@
package io.legado.app.model.analyzeRule package io.legado.app.model.analyzeRule
import androidx.annotation.Keep
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@Keep
object AnalyzeByRegex { object AnalyzeByRegex {
fun getElement(res: String, regs: Array<String>, index: Int = 0): List<String>? { fun getElement(res: String, regs: Array<String>, index: Int = 0): List<String>? {

@ -1,6 +1,7 @@
package io.legado.app.model.analyzeRule package io.legado.app.model.analyzeRule
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.Keep
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -9,6 +10,7 @@ import org.seimicrawler.xpath.JXDocument
import org.seimicrawler.xpath.JXNode import org.seimicrawler.xpath.JXNode
import java.util.* import java.util.*
@Keep
class AnalyzeByXPath { class AnalyzeByXPath {
private var jxDocument: JXDocument? = null private var jxDocument: JXDocument? = null
private var jxNode: JXNode? = null private var jxNode: JXNode? = null

@ -3,7 +3,7 @@ package io.legado.app.model.analyzeRule
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.Pattern.JS_PATTERN import io.legado.app.constant.AppPattern.JS_PATTERN
import io.legado.app.data.entities.BaseBook import io.legado.app.data.entities.BaseBook
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions

@ -4,8 +4,8 @@ import android.annotation.SuppressLint
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.Pattern.EXP_PATTERN import io.legado.app.constant.AppPattern.EXP_PATTERN
import io.legado.app.constant.Pattern.JS_PATTERN import io.legado.app.constant.AppPattern.JS_PATTERN
import io.legado.app.data.entities.BaseBook import io.legado.app.data.entities.BaseBook
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.help.http.* import io.legado.app.help.http.*

@ -1,6 +1,5 @@
package io.legado.app.model.rss package io.legado.app.model.rss
import io.legado.app.constant.RSSKeywords
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.model.Debug import io.legado.app.model.Debug
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
@ -34,26 +33,26 @@ object RssParser {
// Start parsing the item // Start parsing the item
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
when { when {
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM, true) -> xmlPullParser.name.equals(RSS_ITEM, true) ->
insideItem = true insideItem = true
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_TITLE, true) -> xmlPullParser.name.equals(RSS_ITEM_TITLE, true) ->
if (insideItem) currentArticle.title = xmlPullParser.nextText().trim() if (insideItem) currentArticle.title = xmlPullParser.nextText().trim()
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_LINK, true) -> xmlPullParser.name.equals(RSS_ITEM_LINK, true) ->
if (insideItem) currentArticle.link = xmlPullParser.nextText().trim() if (insideItem) currentArticle.link = xmlPullParser.nextText().trim()
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_THUMBNAIL, true) -> xmlPullParser.name.equals(RSS_ITEM_THUMBNAIL, true) ->
if (insideItem) currentArticle.image = if (insideItem) currentArticle.image =
xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL) xmlPullParser.getAttributeValue(null, RSS_ITEM_URL)
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_ENCLOSURE, true) -> xmlPullParser.name.equals(RSS_ITEM_ENCLOSURE, true) ->
if (insideItem) { if (insideItem) {
val type = val type =
xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_TYPE) xmlPullParser.getAttributeValue(null, RSS_ITEM_TYPE)
if (type != null && type.contains("image/")) { if (type != null && type.contains("image/")) {
currentArticle.image = currentArticle.image =
xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL) xmlPullParser.getAttributeValue(null, RSS_ITEM_URL)
} }
} }
xmlPullParser.name xmlPullParser.name
.equals(RSSKeywords.RSS_ITEM_DESCRIPTION, true) -> .equals(RSS_ITEM_DESCRIPTION, true) ->
if (insideItem) { if (insideItem) {
val description = xmlPullParser.nextText() val description = xmlPullParser.nextText()
currentArticle.description = description.trim() currentArticle.description = description.trim()
@ -61,7 +60,7 @@ object RssParser {
currentArticle.image = getImageUrl(description) currentArticle.image = getImageUrl(description)
} }
} }
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_CONTENT, true) -> xmlPullParser.name.equals(RSS_ITEM_CONTENT, true) ->
if (insideItem) { if (insideItem) {
val content = xmlPullParser.nextText().trim() val content = xmlPullParser.nextText().trim()
currentArticle.content = content currentArticle.content = content
@ -70,7 +69,7 @@ object RssParser {
} }
} }
xmlPullParser.name xmlPullParser.name
.equals(RSSKeywords.RSS_ITEM_PUB_DATE, true) -> .equals(RSS_ITEM_PUB_DATE, true) ->
if (insideItem) { if (insideItem) {
val nextTokenType = xmlPullParser.next() val nextTokenType = xmlPullParser.next()
if (nextTokenType == XmlPullParser.TEXT) { if (nextTokenType == XmlPullParser.TEXT) {
@ -79,7 +78,7 @@ object RssParser {
// Skip to be able to find date inside 'tag' tag // Skip to be able to find date inside 'tag' tag
continue@loop continue@loop
} }
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_TIME, true) -> xmlPullParser.name.equals(RSS_ITEM_TIME, true) ->
if (insideItem) currentArticle.pubDate = xmlPullParser.nextText() if (insideItem) currentArticle.pubDate = xmlPullParser.nextText()
} }
} else if (eventType == XmlPullParser.END_TAG } else if (eventType == XmlPullParser.END_TAG
@ -129,4 +128,17 @@ object RssParser {
} }
return url return url
} }
private const val RSS_ITEM = "item"
private const val RSS_ITEM_TITLE = "title"
private const val RSS_ITEM_LINK = "link"
private const val RSS_ITEM_CATEGORY = "category"
private const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
private const val RSS_ITEM_ENCLOSURE = "enclosure"
private const val RSS_ITEM_DESCRIPTION = "description"
private const val RSS_ITEM_CONTENT = "content:encoded"
private const val RSS_ITEM_PUB_DATE = "pubDate"
private const val RSS_ITEM_TIME = "time"
private const val RSS_ITEM_URL = "url"
private const val RSS_ITEM_TYPE = "type"
} }

@ -1,5 +1,6 @@
package io.legado.app.model.rss package io.legado.app.model.rss
import androidx.annotation.Keep
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
@ -8,6 +9,7 @@ import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
@Keep
object RssParserByRule { object RssParserByRule {
@Throws(Exception::class) @Throws(Exception::class)

@ -2,6 +2,7 @@ package io.legado.app.model.webBook
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.model.Debug import io.legado.app.model.Debug
@ -38,7 +39,7 @@ object BookInfo {
Debug.log(bookSource.bookSourceUrl, "${book.name}") Debug.log(bookSource.bookSourceUrl, "${book.name}")
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
analyzeRule.getString(infoRule.author).let { analyzeRule.getString(infoRule.author).let {
if (it.isNotEmpty()) book.author = it if (it.isNotEmpty()) book.author = it.replace(AppPattern.authorRegex, "")
} }
Debug.log(bookSource.bookSourceUrl, "${book.author}") Debug.log(bookSource.bookSourceUrl, "${book.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")

@ -124,16 +124,16 @@ abstract class BaseReadAloudService : BaseService(),
open fun play() { open fun play() {
pause = false pause = false
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
upNotification() upNotification()
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
} }
@CallSuper @CallSuper
open fun pauseReadAloud(pause: Boolean) { open fun pauseReadAloud(pause: Boolean) {
postEvent(EventBus.ALOUD_STATE, Status.PAUSE)
BaseReadAloudService.pause = pause BaseReadAloudService.pause = pause
upNotification() upNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
postEvent(EventBus.ALOUD_STATE, Status.PAUSE)
} }
@CallSuper @CallSuper

@ -13,7 +13,6 @@ import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import org.jetbrains.anko.toast
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
@ -84,8 +83,6 @@ class HttpReadAloudService : BaseReadAloudService(),
break break
} }
} }
}.onError {
toast("下载朗读文件出错:${it.localizedMessage}")
} }
} }

@ -3,12 +3,12 @@ package io.legado.app.service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.constant.PreferKey
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
@ -96,7 +96,7 @@ class WebService : BaseService() {
} }
private fun getPort(): Int { private fun getPort(): Int {
var port = App.INSTANCE.getPrefInt("webPort", 1122) var port = getPrefInt(PreferKey.webPort, 1122)
if (port > 65530 || port < 1024) { if (port > 65530 || port < 1024) {
port = 1122 port = 1122
} }

@ -11,7 +11,7 @@ import android.widget.SeekBar
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions.bitmapTransform
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
@ -120,19 +120,17 @@ class AudioPlayActivity :
ImageLoader.load(this, path) ImageLoader.load(this, path)
.placeholder(R.drawable.image_cover_default) .placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default) .error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover) .into(iv_cover)
ImageLoader.load(this, path) ImageLoader.load(this, path)
.transition(DrawableTransitionOptions.withCrossFade(1500)) .transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover()) .thumbnail(defaultCover())
.centerCrop() .apply(bitmapTransform(BlurTransformation(this, 25)))
.apply(RequestOptions.bitmapTransform(BlurTransformation(this, 25)))
.into(iv_bg) .into(iv_bg)
} }
private fun defaultCover(): RequestBuilder<Drawable> { private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default) return ImageLoader.load(this, R.drawable.image_cover_default)
.apply(RequestOptions.bitmapTransform(BlurTransformation(this, 25))) .apply(bitmapTransform(BlurTransformation(this, 25)))
} }
private fun playButton() { private fun playButton() {

@ -3,19 +3,24 @@ package io.legado.app.ui.book.changecover
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.Theme
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.dialog_change_source.* import kotlinx.android.synthetic.main.dialog_change_source.*
class ChangeCoverDialog : DialogFragment(), class ChangeCoverDialog : BaseDialogFragment(),
ChangeCoverViewModel.CallBack, Toolbar.OnMenuItemClickListener,
CoverAdapter.CallBack { CoverAdapter.CallBack {
companion object { companion object {
@ -35,7 +40,7 @@ class ChangeCoverDialog : DialogFragment(),
private var callBack: CallBack? = null private var callBack: CallBack? = null
private lateinit var viewModel: ChangeCoverViewModel private lateinit var viewModel: ChangeCoverViewModel
override lateinit var adapter: CoverAdapter lateinit var adapter: CoverAdapter
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -51,30 +56,57 @@ class ChangeCoverDialog : DialogFragment(),
): View? { ): View? {
callBack = activity as? CallBack callBack = activity as? CallBack
viewModel = getViewModel(ChangeCoverViewModel::class.java) viewModel = getViewModel(ChangeCoverViewModel::class.java)
viewModel.callBack = this
return inflater.inflate(R.layout.dialog_change_cover, container) return inflater.inflate(R.layout.dialog_change_cover, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it
})
tool_bar.setTitle(R.string.change_cover_source) tool_bar.setTitle(R.string.change_cover_source)
arguments?.let { bundle -> viewModel.initData(arguments)
bundle.getString("name")?.let { initMenu()
viewModel.name = it initView()
}
bundle.getString("author")?.let {
viewModel.author = it
} }
private fun initMenu() {
tool_bar.inflateMenu(R.menu.change_cover)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
} }
private fun initView() {
recycler_view.layoutManager = GridLayoutManager(requireContext(), 3) recycler_view.layoutManager = GridLayoutManager(requireContext(), 3)
adapter = CoverAdapter(requireContext(), this) adapter = CoverAdapter(requireContext(), this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
viewModel.initData() viewModel.loadDbSearchBook()
} }
override fun observeLiveBus() {
super.observeLiveBus()
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it
if (it) {
stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp)
} else {
stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp)
}
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
})
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer {
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.setItems(it)
diffResult.dispatchUpdatesTo(adapter)
})
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_stop -> viewModel.stopSearch()
}
return false
}
private val stopMenuItem: MenuItem?
get() = tool_bar.menu.findItem(R.id.menu_stop)
override fun changeTo(coverUrl: String) { override fun changeTo(coverUrl: String) {
callBack?.coverChangeTo(coverUrl) callBack?.coverChangeTo(coverUrl)
dismiss() dismiss()

@ -1,35 +1,51 @@
package io.legado.app.ui.book.changecover package io.legado.app.ui.book.changecover
import android.app.Application import android.app.Application
import android.os.Bundle
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors import java.util.concurrent.Executors
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
var callBack: CallBack? = null
var name: String = "" var name: String = ""
var author: String = "" var author: String = ""
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
val searchBooksLiveData = MutableLiveData<List<SearchBook>>()
private val searchBooks = ArrayList<SearchBook>()
fun initData() { fun initData(arguments: Bundle?) {
execute { arguments?.let { bundle ->
App.db.searchBookDao().getEnableHasCover(name, author) bundle.getString("name")?.let {
}.onSuccess { name = it
it?.let { }
callBack?.adapter?.setItems(it) bundle.getString("author")?.let {
author = it.replace(AppPattern.authorRegex, "")
}
}
} }
}.onFinally {
fun loadDbSearchBook() {
execute {
App.db.searchBookDao().getEnableHasCover(name, author).let {
searchBooks.addAll(it)
if (it.size <= 1) {
searchBooksLiveData.postValue(searchBooks)
search() search()
} else {
searchBooksLiveData.postValue(searchBooks)
}
}
} }
} }
@ -48,12 +64,9 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
&& !searchBook.coverUrl.isNullOrEmpty() && !searchBook.coverUrl.isNullOrEmpty()
) { ) {
App.db.searchBookDao().insert(searchBook) App.db.searchBookDao().insert(searchBook)
callBack?.adapter?.let { adapter -> if (!searchBooks.contains(searchBook)) {
if (!adapter.getItems().contains(searchBook)) { searchBooks.add(searchBook)
withContext(Dispatchers.Main) { searchBooksLiveData.postValue(searchBooks)
adapter.addItem(searchBook)
}
}
} }
} }
} }
@ -66,12 +79,17 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
} }
fun stopSearch() {
if (task?.isActive == true) {
task?.cancel()
} else {
search()
}
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchPool.close() searchPool.close()
} }
interface CallBack {
var adapter: CoverAdapter
}
} }

@ -0,0 +1,52 @@
package io.legado.app.ui.book.changecover
import android.os.Bundle
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.data.entities.SearchBook
class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems: List<SearchBook>) :
DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldItems.size
}
override fun getNewListSize(): Int {
return newItems.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return oldItem.bookUrl == newItem.bookUrl
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
if (oldItem.originName != newItem.originName) {
return false
}
if (oldItem.coverUrl != newItem.coverUrl) {
return false
}
return true
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle()
if (oldItem.originName != newItem.originName) {
payload.putString("name", newItem.originName)
}
if (oldItem.coverUrl != newItem.coverUrl) {
payload.putString("coverUrl", newItem.coverUrl)
}
if (payload.isEmpty) {
return null
}
return payload
}
}

@ -8,13 +8,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -27,7 +27,7 @@ import io.legado.app.utils.putPrefBoolean
import kotlinx.android.synthetic.main.dialog_change_source.* import kotlinx.android.synthetic.main.dialog_change_source.*
class ChangeSourceDialog : DialogFragment(), class ChangeSourceDialog : BaseDialogFragment(),
Toolbar.OnMenuItemClickListener, Toolbar.OnMenuItemClickListener,
ChangeSourceAdapter.CallBack { ChangeSourceAdapter.CallBack {
@ -67,8 +67,7 @@ class ChangeSourceDialog : DialogFragment(),
return inflater.inflate(R.layout.dialog_change_source, container) return inflater.inflate(R.layout.dialog_change_source, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.initData(arguments) viewModel.initData(arguments)
showTitle() showTitle()
initMenu() initMenu()
@ -76,7 +75,6 @@ class ChangeSourceDialog : DialogFragment(),
initSearchView() initSearchView()
initLiveData() initLiveData()
viewModel.loadDbSearchBook() viewModel.loadDbSearchBook()
viewModel.search()
} }
private fun showTitle() { private fun showTitle() {
@ -113,15 +111,16 @@ class ChangeSourceDialog : DialogFragment(),
} }
private fun initSearchView() { private fun initSearchView() {
search_view.setOnCloseListener { val searchView = tool_bar.menu.findItem(R.id.menu_screen).actionView as SearchView
searchView.setOnCloseListener {
showTitle() showTitle()
false false
} }
search_view.setOnSearchClickListener { searchView.setOnSearchClickListener {
tool_bar.title = "" tool_bar.title = ""
tool_bar.subtitle = "" tool_bar.subtitle = ""
} }
search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
return false return false
} }
@ -137,6 +136,12 @@ class ChangeSourceDialog : DialogFragment(),
private fun initLiveData() { private fun initLiveData() {
viewModel.searchStateData.observe(viewLifecycleOwner, Observer { viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it refresh_progress_bar.isAutoLoading = it
if (it) {
stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp)
} else {
stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp)
}
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
}) })
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer { viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer {
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it))
@ -145,12 +150,16 @@ class ChangeSourceDialog : DialogFragment(),
}) })
} }
private val stopMenuItem: MenuItem?
get() = tool_bar.menu.findItem(R.id.menu_stop)
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_load_toc -> { R.id.menu_load_toc -> {
putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked) putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked)
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
} }
R.id.menu_stop -> viewModel.stopSearch()
} }
return false return false
} }

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
@ -35,7 +36,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
name = it name = it
} }
bundle.getString("author")?.let { bundle.getString("author")?.let {
author = it author = it.replace(AppPattern.authorRegex, "")
} }
} }
} }
@ -44,10 +45,15 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute { execute {
App.db.searchBookDao().getByNameAuthorEnable(name, author).let { App.db.searchBookDao().getByNameAuthorEnable(name, author).let {
searchBooks.addAll(it) searchBooks.addAll(it)
if (it.size <= 1) {
upAdapter()
search()
} else {
upAdapter() upAdapter()
} }
} }
} }
}
private fun upAdapter() { private fun upAdapter() {
val books = searchBooks.toList() val books = searchBooks.toList()
@ -145,6 +151,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
fun stopSearch() {
if (task?.isActive == true) {
task?.cancel()
} else {
search()
}
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchPool.close() searchPool.close()

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -55,7 +54,6 @@ class BookInfoActivity :
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
tv_intro.movementMethod = ScrollingMovementMethod.getInstance()
viewModel.bookData.observe(this, Observer { showBook(it) }) viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.chapterListData.observe(this, Observer { upLoading(false, it) }) viewModel.chapterListData.observe(this, Observer { upLoading(false, it) })
viewModel.initData(intent) viewModel.initData(intent)
@ -105,7 +103,7 @@ class BookInfoActivity :
private fun showBook(book: Book) { private fun showBook(book: Book) {
showCover(book) showCover(book)
tv_name.text = book.name tv_name.text = book.name
tv_author.text = getString(R.string.author_show, book.author) tv_author.text = getString(R.string.author_show, book.getRealAuthor())
tv_origin.text = getString(R.string.origin_show, book.originName) tv_origin.text = getString(R.string.origin_show, book.originName)
tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle) tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle)
tv_toc.text = getString(R.string.toc_s, getString(R.string.loading)) tv_toc.text = getString(R.string.toc_s, getString(R.string.loading))
@ -126,7 +124,6 @@ class BookInfoActivity :
ImageLoader.load(this, book.getDisplayCover()) ImageLoader.load(this, book.getDisplayCover())
.transition(DrawableTransitionOptions.withCrossFade(1500)) .transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover()) .thumbnail(defaultCover())
.centerCrop()
.apply(bitmapTransform(BlurTransformation(this, 25))) .apply(bitmapTransform(BlurTransformation(this, 25)))
.into(bg_book) //模糊、渐变、缩小效果 .into(bg_book) //模糊、渐变、缩小效果
} }

@ -152,6 +152,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
PaddingConfigDialog().show(supportFragmentManager, "paddingConfig") PaddingConfigDialog().show(supportFragmentManager, "paddingConfig")
} }
fun showTypeConfig() {
ReadTypeDialog().show(supportFragmentManager, "readTypeDialog")
}
fun showBgTextConfig() { fun showBgTextConfig() {
BgTextConfigDialog().show(supportFragmentManager, "bgTextConfig") BgTextConfigDialog().show(supportFragmentManager, "bgTextConfig")
} }

@ -6,6 +6,7 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.Gravity import android.view.Gravity
@ -13,11 +14,11 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
@ -31,9 +32,8 @@ import kotlinx.android.synthetic.main.dialog_read_bg_text.*
import kotlinx.android.synthetic.main.item_bg_image.view.* import kotlinx.android.synthetic.main.item_bg_image.view.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onCheckedChange
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import java.io.File
class BgTextConfigDialog : DialogFragment() { class BgTextConfigDialog : BaseDialogFragment() {
companion object { companion object {
const val TEXT_COLOR = 121 const val TEXT_COLOR = 121
@ -69,8 +69,7 @@ class BgTextConfigDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_read_bg_text, container) return inflater.inflate(R.layout.dialog_read_bg_text, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData() initData()
initView() initView()
} }
@ -171,19 +170,28 @@ class BgTextConfigDialog : DialogFragment() {
resultSelectBg -> { resultSelectBg -> {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
data?.data?.let { uri -> data?.data?.let { uri ->
setBgFromUri(uri)
}
}
}
}
}
private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentPath()) { if (uri.toString().isContentPath()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri) val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.let { doc?.name?.let {
var file = requireContext().getExternalFilesDir(null) var file = requireContext().getExternalFilesDir(null)
?: requireContext().filesDir ?: requireContext().filesDir
file = file = FileUtils.createFileIfNotExist(file, it, "bg")
FileUtils.createFileIfNotExist(file.absolutePath + File.separator + "bg" + File.separator + doc.name) kotlin.runCatching {
DocumentUtils.readBytes(requireContext(), uri)?.let { DocumentUtils.readBytes(requireContext(), doc.uri)
file.writeBytes(it) }.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
ReadBookConfig.durConfig.setBg(2, file.absolutePath) ReadBookConfig.durConfig.setBg(2, file.absolutePath)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} } ?: toast("获取文件出错")
} }
} else { } else {
PermissionsCompat.Builder(this) PermissionsCompat.Builder(this)
@ -202,8 +210,4 @@ class BgTextConfigDialog : DialogFragment() {
.request() .request()
} }
} }
}
}
}
}
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -11,6 +12,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.dp import io.legado.app.utils.dp
import io.legado.app.utils.gone
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_read_padding.* import kotlinx.android.synthetic.main.dialog_read_padding.*
@ -46,19 +48,18 @@ class PaddingConfigDialog : DialogFragment() {
initView() initView()
} }
override fun onDestroy() { override fun onDismiss(dialog: DialogInterface) {
super.onDestroy() super.onDismiss(dialog)
ReadBookConfig.save() ReadBookConfig.save()
} }
private fun initData() = with(ReadBookConfig) { private fun initData() = ReadBookConfig.apply {
if (hideStatusBar) { if (hideStatusBar) {
tv_header_padding.visible() ll_header_padding.visible()
dsb_header_padding_top.visible()
dsb_header_padding_bottom.visible()
dsb_header_padding_left.visible()
dsb_header_padding_right.visible()
tv_body_padding.setPadding(0, 10.dp, 0, 10.dp) tv_body_padding.setPadding(0, 10.dp, 0, 10.dp)
} else {
ll_header_padding.gone()
tv_body_padding.setPadding(0, 0.dp, 0, 10.dp)
} }
//正文 //正文
dsb_padding_top.progress = paddingTop dsb_padding_top.progress = paddingTop
@ -75,6 +76,7 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_bottom.progress = footerPaddingBottom dsb_footer_padding_bottom.progress = footerPaddingBottom
dsb_footer_padding_left.progress = footerPaddingLeft dsb_footer_padding_left.progress = footerPaddingLeft
dsb_footer_padding_right.progress = footerPaddingRight dsb_footer_padding_right.progress = footerPaddingRight
cb_show_line.isChecked = showFooterLine
} }
private fun initView() = with(ReadBookConfig) { private fun initView() = with(ReadBookConfig) {
@ -129,6 +131,12 @@ class PaddingConfigDialog : DialogFragment() {
footerPaddingRight = it footerPaddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
cb_show_line.onCheckedChangeListener = { cb, isChecked ->
if (cb.isPressed) {
showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true)
}
}
} }
} }

@ -7,8 +7,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
@ -23,7 +23,7 @@ import kotlinx.android.synthetic.main.dialog_read_aloud.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadAloudDialog : DialogFragment() { class ReadAloudDialog : BaseDialogFragment() {
var callBack: CallBack? = null var callBack: CallBack? = null
override fun onStart() { override fun onStart() {
@ -53,21 +53,17 @@ class ReadAloudDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_read_aloud, container) return inflater.inflate(R.layout.dialog_read_aloud, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ll_bottom_bg.setBackgroundColor(requireContext().bottomBackground) ll_bottom_bg.setBackgroundColor(requireContext().bottomBackground)
initData()
initOnChange() initOnChange()
initData()
initOnClick() initOnClick()
} }
private fun initData() { private fun initData() {
observeEvent<Int>(EventBus.ALOUD_STATE) { upPlayState() }
observeEvent<Int>(EventBus.TTS_DS) { seek_timer.progress = it }
upPlayState() upPlayState()
upTimerText(BaseReadAloudService.timeMinute)
seek_timer.progress = BaseReadAloudService.timeMinute seek_timer.progress = BaseReadAloudService.timeMinute
tv_timer.text =
requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute)
cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked
seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate
@ -94,7 +90,7 @@ class ReadAloudDialog : DialogFragment() {
}) })
seek_timer.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seek_timer.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
tv_timer.text = requireContext().getString(R.string.timer_m, progress) upTimerText(progress)
} }
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
@ -131,6 +127,10 @@ class ReadAloudDialog : DialogFragment() {
} }
} }
private fun upTimerText(timeMinute: Int) {
tv_timer.text = requireContext().getString(R.string.timer_m, timeMinute)
}
private fun upTtsSpeechRate() { private fun upTtsSpeechRate() {
ReadAloud.upTtsSpeechRate(requireContext()) ReadAloud.upTtsSpeechRate(requireContext())
if (!BaseReadAloudService.pause) { if (!BaseReadAloudService.pause) {
@ -139,6 +139,11 @@ class ReadAloudDialog : DialogFragment() {
} }
} }
override fun observeLiveBus() {
observeEvent<Int>(EventBus.ALOUD_STATE) { upPlayState() }
observeEvent<Int>(EventBus.TTS_DS) { seek_timer.progress = it }
}
interface CallBack { interface CallBack {
fun showMenuBar() fun showMenuBar()
fun openChapterList() fun openChapterList()

@ -1,5 +1,6 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.Gravity import android.view.Gravity
@ -32,6 +33,7 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
val callBack get() = activity as? ReadBookActivity val callBack get() = activity as? ReadBookActivity
lateinit var titleModes: Array<String>
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -61,24 +63,19 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
titleModes = requireContext().resources.getStringArray(R.array.title_mode)
initView() initView()
initData() initData()
initViewEvent() initViewEvent()
} }
override fun onDestroy() { override fun onDismiss(dialog: DialogInterface) {
super.onDestroy() super.onDismiss(dialog)
ReadBookConfig.save() ReadBookConfig.save()
} }
private fun initView() { private fun initView() {
root_view.setBackgroundColor(requireContext().bottomBackground) root_view.setBackgroundColor(requireContext().bottomBackground)
dsb_text_size.valueFormat = {
(it + 5).toString()
}
dsb_text_letter_spacing.valueFormat = {
((it - 50) / 100f).toString()
}
} }
private fun initData() { private fun initData() {
@ -97,13 +94,13 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
chinese_converter.onChanged { chinese_converter.onChanged {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
tv_title_center.onClick { tv_title_mode.onClick {
ReadBookConfig.apply { requireContext().selector("标题模式", titleModes.toList()) { _, index ->
titleCenter = !titleCenter ReadBookConfig.titleMode = index
tv_title_center.isSelected = titleCenter tv_title_mode.text = titleModes[index]
}
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}
tv_text_bold.onClick { tv_text_bold.onClick {
ReadBookConfig.apply { ReadBookConfig.apply {
textBold = !textBold textBold = !textBold
@ -127,21 +124,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
dismiss() dismiss()
callBack?.showPaddingConfig() callBack?.showPaddingConfig()
} }
dsb_text_size.onChanged = { tv_type.onClick {
ReadBookConfig.textSize = it + 5 dismiss()
postEvent(EventBus.UP_CONFIG, true) callBack?.showTypeConfig()
}
dsb_text_letter_spacing.onChanged = {
ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true)
}
dsb_line_size.onChanged = {
ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_paragraph_spacing.onChanged = {
ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true)
} }
rg_page_anim.onCheckedChange { _, checkedId -> rg_page_anim.onCheckedChange { _, checkedId ->
rg_page_anim.getIndexById(checkedId).let { rg_page_anim.getIndexById(checkedId).let {
@ -156,6 +141,21 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }
iv_default1.onClick {
ReadBookConfig.lineSpacingExtra = 16
ReadBookConfig.paragraphSpacing = 6
postEvent(EventBus.UP_CONFIG, true)
}
iv_default2.onClick {
ReadBookConfig.lineSpacingExtra = 13
ReadBookConfig.paragraphSpacing = 3
postEvent(EventBus.UP_CONFIG, true)
}
iv_default3.onClick {
ReadBookConfig.lineSpacingExtra = 10
ReadBookConfig.paragraphSpacing = 0
postEvent(EventBus.UP_CONFIG, true)
}
bg0.onClick { changeBg(0) } bg0.onClick { changeBg(0) }
bg0.onLongClick { showBgTextConfig(0) } bg0.onLongClick { showBgTextConfig(0) }
bg1.onClick { changeBg(1) } bg1.onClick { changeBg(1) }
@ -187,12 +187,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun upStyle() { private fun upStyle() {
ReadBookConfig.let { ReadBookConfig.let {
tv_title_center.isSelected = it.titleCenter tv_title_mode.text = titleModes.getOrElse(it.titleMode) { titleModes[0] }
tv_text_bold.isSelected = it.textBold tv_text_bold.isSelected = it.textBold
dsb_text_size.progress = it.textSize - 5
dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50
dsb_line_size.progress = it.lineSpacingExtra
dsb_paragraph_spacing.progress = it.paragraphSpacing
} }
} }

@ -0,0 +1,91 @@
package io.legado.app.ui.book.read.config
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.postEvent
import kotlinx.android.synthetic.main.dialog_read_type.*
class ReadTypeDialog : BaseDialogFragment() {
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
val attr = it.attributes
attr.dimAmount = 0.0f
it.attributes = attr
it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_read_type, container)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
ReadBookConfig.save()
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
initView()
initViewEvent()
upStyle()
}
private fun initView() {
dsb_text_size.valueFormat = {
(it + 5).toString()
}
dsb_text_letter_spacing.valueFormat = {
((it - 50) / 100f).toString()
}
dsb_line_size.valueFormat = { ((it - 10) / 10f).toString() }
dsb_paragraph_spacing.valueFormat = { (it / 10f).toString() }
}
private fun initViewEvent() {
dsb_text_size.onChanged = {
ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true)
}
dsb_text_letter_spacing.onChanged = {
ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true)
}
dsb_line_size.onChanged = {
ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_paragraph_spacing.onChanged = {
ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true)
}
}
private fun upStyle() {
ReadBookConfig.let {
dsb_text_size.progress = it.textSize - 5
dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50
dsb_line_size.progress = it.lineSpacingExtra
dsb_paragraph_spacing.progress = it.paragraphSpacing
}
}
}

@ -15,6 +15,7 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
@ -27,9 +28,10 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.cancelButton import io.legado.app.lib.dialogs.cancelButton
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_toc_regex.* import kotlinx.android.synthetic.main.dialog_toc_regex.*
import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.* import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.*
import kotlinx.android.synthetic.main.item_toc_regex.view.* import kotlinx.android.synthetic.main.item_toc_regex.view.*
@ -40,11 +42,12 @@ import java.util.*
class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private val importTocRuleKey = "tocRuleUrl"
private lateinit var adapter: TocRegexAdapter private lateinit var adapter: TocRegexAdapter
private var tocRegexLiveData: LiveData<List<TxtTocRule>>? = null private var tocRegexLiveData: LiveData<List<TxtTocRule>>? = null
var selectedName: String? = null var selectedName: String? = null
private var durRegex: String? = null private var durRegex: String? = null
lateinit var viewModel: TocRegexViewModel
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -58,11 +61,11 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
viewModel = getViewModel(TocRegexViewModel::class.java)
return inflater.inflate(R.layout.dialog_toc_regex, container) return inflater.inflate(R.layout.dialog_toc_regex, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
durRegex = arguments?.getString("tocRegex") durRegex = arguments?.getString("tocRegex")
tool_bar.setTitle(R.string.txt_toc_regex) tool_bar.setTitle(R.string.txt_toc_regex)
tool_bar.inflateMenu(R.menu.txt_toc_regex) tool_bar.inflateMenu(R.menu.txt_toc_regex)
@ -122,17 +125,51 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> editRule() R.id.menu_add -> editRule()
R.id.menu_default -> importDefault() R.id.menu_default -> viewModel.importDefault()
R.id.menu_import -> showImportDialog()
} }
return false return false
} }
private fun importDefault() { @SuppressLint("InflateParams")
launch(IO) { private fun showImportDialog() {
AnalyzeTxtFile.getDefaultRules().let { val aCache = ACache.get(requireContext(), cacheDir = false)
App.db.txtTocRule().insert(*it.toTypedArray()) val defaultUrl = "https://gitee.com/fisher52/YueDuJson/raw/master/myTxtChapterRule.json"
val cacheUrls: MutableList<String> = aCache
.getAsString(importTocRuleKey)
?.splitNotBlank(",")
?.toMutableList()
?: mutableListOf()
if (!cacheUrls.contains(defaultUrl)) {
cacheUrls.add(0, defaultUrl)
}
requireContext().alert(titleResource = R.string.import_book_source_on_line) {
var editText: AutoCompleteTextView? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = this.edit_view
edit_view.setFilterValues(cacheUrls)
edit_view.delCallBack = {
cacheUrls.remove(it)
aCache.put(importTocRuleKey, cacheUrls.joinToString(","))
}
} }
} }
okButton {
val text = editText?.text?.toString()
text?.let {
if (!cacheUrls.contains(it)) {
cacheUrls.add(0, it)
aCache.put(importTocRuleKey, cacheUrls.joinToString(","))
}
Snackbar.make(tool_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importOnLine(it) { msg ->
tool_bar.snackbar(msg)
}
}
}
cancelButton()
}.show().applyTint()
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -152,25 +189,13 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
rootView?.apply { rootView?.apply {
tocRule.name = tv_rule_name.text.toString() tocRule.name = tv_rule_name.text.toString()
tocRule.rule = tv_rule_regex.text.toString() tocRule.rule = tv_rule_regex.text.toString()
saveRule(tocRule, rule) viewModel.saveRule(tocRule, rule)
} }
} }
cancelButton() cancelButton()
}.show().applyTint() }.show().applyTint()
} }
private fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) {
launch(IO) {
if (rule.serialNumber < 0) {
rule.serialNumber = adapter.getItems().lastOrNull()?.serialNumber ?: 0 + 1
}
oldRule?.let {
App.db.txtTocRule().delete(oldRule)
}
App.db.txtTocRule().insert(rule)
}
}
inner class TocRegexAdapter(context: Context) : inner class TocRegexAdapter(context: Context) :
SimpleRecyclerAdapter<TxtTocRule>(context, R.layout.item_toc_regex), SimpleRecyclerAdapter<TxtTocRule>(context, R.layout.item_toc_regex),
ItemTouchCallback.OnItemTouchCallbackListener { ItemTouchCallback.OnItemTouchCallbackListener {
@ -241,7 +266,6 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
} }
} }
companion object { companion object {
fun show(fragmentManager: FragmentManager, tocRegex: String? = null) { fun show(fragmentManager: FragmentManager, tocRegex: String? = null) {
val dialog = TocRegexDialog() val dialog = TocRegexDialog()

@ -0,0 +1,48 @@
package io.legado.app.ui.book.read.config
import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.http.HttpHelper
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
class TocRegexViewModel(application: Application) : BaseViewModel(application) {
fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) {
execute {
if (rule.serialNumber < 0) {
rule.serialNumber = App.db.txtTocRule().lastOrderNum + 1
}
oldRule?.let {
App.db.txtTocRule().delete(oldRule)
}
App.db.txtTocRule().insert(rule)
}
}
fun importDefault() {
execute {
AnalyzeTxtFile.getDefaultRules().let {
App.db.txtTocRule().insert(*it.toTypedArray())
}
}
}
fun importOnLine(url: String, finally: (msg: String) -> Unit) {
execute {
HttpHelper.simpleGetAsync(url)?.let { json ->
GSON.fromJsonArray<TxtTocRule>(json)?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
}
}
}.onSuccess {
finally("导入成功")
}.onError {
finally("导入失败")
}
}
}

@ -13,10 +13,7 @@ import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.dp import io.legado.app.utils.*
import io.legado.app.utils.getPrefString
import io.legado.app.utils.removePref
import io.legado.app.utils.toStringArray
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -51,34 +48,16 @@ object ChapterProvider {
val pageLines = arrayListOf<Int>() val pageLines = arrayListOf<Int>()
val pageLengths = arrayListOf<Int>() val pageLengths = arrayListOf<Int>()
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
var surplusText = content val contents = content.split("\n")
var durY = 0f var durY = 0f
textPages.add(TextPage()) textPages.add(TextPage())
while (surplusText.isNotEmpty()) { for ((index, text) in contents.withIndex()) {
if (textPages.first().textLines.isEmpty()) { val isTitle = index == 0
//title if (isTitle && ReadBookConfig.titleMode == 2) {
val end = surplusText.indexOf("\n") continue
if (end > 0) {
val title = surplusText.substring(0, end)
surplusText = surplusText.substring(end + 1)
durY = setTypeText(
title, durY, textPages, pageLines, pageLengths, stringBuilder, true
)
}
} else {
//正文
val end = surplusText.indexOf("\n")
val text: String
if (end >= 0) {
text = surplusText.substring(0, end)
surplusText = surplusText.substring(end + 1)
} else {
text = surplusText
surplusText = ""
} }
durY = durY =
setTypeText(text, durY, textPages, pageLines, pageLengths, stringBuilder, false) setTypeText(text, durY, textPages, pageLines, pageLengths, stringBuilder, isTitle)
}
} }
textPages.last().height = durY + 20.dp textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString() textPages.last().text = stringBuilder.toString()
@ -125,44 +104,45 @@ object ChapterProvider {
Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true
) )
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {
textPages.last().height = durY
val textLine = TextLine(isTitle = isTitle) val textLine = TextLine(isTitle = isTitle)
if (durY + textPaint.textHeight < visibleHeight) {
textPages.last().textLines.add(textLine)
durY += textPaint.textHeight + lineSpacingExtra
} else {
textPages.last().text = stringBuilder.toString()
stringBuilder.clear()
pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length)
//新页面
durY = textPaint.textHeight + lineSpacingExtra
textPages.add(TextPage())
textPages.last().textLines.add(textLine)
}
textLine.lineBottom = paddingTop + durY - lineSpacingExtra
textLine.lineBase = textLine.lineBottom - textPaint.fontMetrics.descent
textLine.lineTop = textLine.lineBottom - textPaint.textHeight
val words = val words =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words)
textLine.text = words textLine.text = words
val desiredWidth = layout.getLineWidth(lineIndex) val desiredWidth = layout.getLineWidth(lineIndex)
var isLastLine = false
if (lineIndex == 0 && layout.lineCount > 1 && !isTitle) { if (lineIndex == 0 && layout.lineCount > 1 && !isTitle) {
//第一行 //第一行
addCharsToLineFirst(textLine, words, textPaint, desiredWidth) addCharsToLineFirst(textLine, words, textPaint, desiredWidth)
} else if (lineIndex == layout.lineCount - 1) { } else if (lineIndex == layout.lineCount - 1) {
//最后一行 //最后一行
val x = if (isTitle && ReadBookConfig.titleCenter) isLastLine = true
val x = if (isTitle && ReadBookConfig.titleMode == 1)
(visibleWidth - layout.getLineWidth(lineIndex)) / 2 (visibleWidth - layout.getLineWidth(lineIndex)) / 2
else 0f else 0f
addCharsToLineLast(textLine, words, stringBuilder, textPaint, x) addCharsToLineLast(textLine, words, textPaint, x)
} else { } else {
//中间行 //中间行
addCharsToLineMiddle(textLine, words, textPaint, desiredWidth, 0f) addCharsToLineMiddle(textLine, words, textPaint, desiredWidth, 0f)
} }
if (durY + textPaint.textHeight > visibleHeight) {
//当前页面结束,设置各种值
textPages.last().text = stringBuilder.toString()
pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length)
textPages.last().height = durY
//新建页面
textPages.add(TextPage())
stringBuilder.clear()
durY = 0f
}
stringBuilder.append(words)
if (isLastLine) stringBuilder.append("\n")
textPages.last().textLines.add(textLine)
textLine.upTopBottom(durY, textPaint)
durY += textPaint.textHeight * lineSpacingExtra / 10f
textPages.last().height = durY
} }
durY += paragraphSpacing durY += textPaint.textHeight * paragraphSpacing / 10f
return durY return durY
} }
@ -223,11 +203,9 @@ object ChapterProvider {
private fun addCharsToLineLast( private fun addCharsToLineLast(
textLine: TextLine, textLine: TextLine,
words: String, words: String,
stringBuilder: StringBuilder,
textPaint: TextPaint, textPaint: TextPaint,
startX: Float startX: Float
) { ) {
stringBuilder.append("\n")
textLine.text = "$words\n" textLine.text = "$words\n"
var x = startX var x = startX
words.toStringArray().forEach { words.toStringArray().forEach {
@ -291,10 +269,10 @@ object ChapterProvider {
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold) contentPaint.typeface = Typeface.create(typeface, bold)
//间距 //间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra.dp lineSpacingExtra = ReadBookConfig.lineSpacingExtra
paragraphSpacing = ReadBookConfig.paragraphSpacing.dp paragraphSpacing = ReadBookConfig.paragraphSpacing
titlePaint.textSize = (ReadBookConfig.textSize + 2).dp.toFloat() titlePaint.textSize = (ReadBookConfig.textSize + 2).sp.toFloat()
contentPaint.textSize = ReadBookConfig.textSize.dp.toFloat() contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
upSize() upSize()
} }
@ -311,8 +289,6 @@ object ChapterProvider {
visibleBottom = paddingTop + visibleHeight visibleBottom = paddingTop + visibleHeight
} }
private val TextPaint.textHeight: Float val TextPaint.textHeight: Float
get() { get() = this.fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading
return this.fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading
}
} }

@ -60,6 +60,7 @@ class ContentView(context: Context) : FrameLayout(context) {
footerPaddingRight.dp, footerPaddingRight.dp,
footerPaddingBottom.dp footerPaddingBottom.dp
) )
vw_bottom_divider.visible(showFooterLine)
content_text_view.upVisibleRect() content_text_view.upVisibleRect()
durConfig.textColor().let { durConfig.textColor().let {
tv_top_left.setTextColor(it) tv_top_left.setTextColor(it)

@ -18,14 +18,16 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (!isRunning) return
val offsetX = touchX - startX val offsetX = touchX - startX
if ((mDirection == Direction.NEXT && offsetX > 0) if ((mDirection == Direction.NEXT && offsetX > 0)
|| (mDirection == Direction.PREV && offsetX < 0) || (mDirection == Direction.PREV && offsetX < 0)
) return ) {
return
}
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (!isRunning) return
if (mDirection == Direction.PREV) { if (mDirection == Direction.PREV) {
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) bitmapMatrix.setTranslate(distanceX, 0.toFloat())
curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }

@ -65,13 +65,13 @@ abstract class PageDelegate(protected val pageView: PageView) :
var mDirection = Direction.NONE var mDirection = Direction.NONE
var isCancel = false var isCancel = false
var isRunning = false var isRunning = false
var isStarted = false private var isStarted = false
var isTextSelected = false var isTextSelected = false
var selectedOnDown = false private var selectedOnDown = false
var firstRelativePage = 0 private var firstRelativePage = 0
var firstLineIndex: Int = 0 private var firstLineIndex: Int = 0
var firstCharIndex: Int = 0 private var firstCharIndex: Int = 0
init { init {
curPage.resetPageOffset() curPage.resetPageOffset()
@ -127,11 +127,13 @@ abstract class PageDelegate(protected val pageView: PageView) :
} }
private fun stopScroll() { private fun stopScroll() {
isStarted = false
pageView.post {
isMoved = false isMoved = false
isRunning = false isRunning = false
isStarted = false
pageView.invalidate() pageView.invalidate()
} }
}
open fun setViewSize(width: Int, height: Int) { open fun setViewSize(width: Int, height: Int) {
viewWidth = width viewWidth = width
@ -194,7 +196,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
@CallSuper @CallSuper
open fun onTouch(event: MotionEvent) { open fun onTouch(event: MotionEvent) {
if (isStarted) return if (isStarted) return
detector.setIsLongpressEnabled(false)
if (!detector.onTouchEvent(event)) { if (!detector.onTouchEvent(event)) {
//GestureDetector.onFling小幅移动不会触发,所以要自己判断 //GestureDetector.onFling小幅移动不会触发,所以要自己判断
when (event.action) { when (event.action) {

@ -1,5 +1,9 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
import android.text.TextPaint
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.ChapterProvider.textHeight
data class TextLine( data class TextLine(
var text: String = "", var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(), val textChars: ArrayList<TextChar> = arrayListOf(),
@ -10,6 +14,12 @@ data class TextLine(
var isReadAloud: Boolean = false var isReadAloud: Boolean = false
) { ) {
fun upTopBottom(durY: Float, textPaint: TextPaint) {
lineTop = ChapterProvider.paddingTop + durY
lineBottom = lineTop + textPaint.textHeight
lineBase = lineBottom - textPaint.fontMetrics.descent
}
fun addTextChar(charData: String, start: Float, end: Float) { fun addTextChar(charData: String, start: Float, end: Float) {
textChars.add(TextChar(charData, start = start, end = end)) textChars.add(TextChar(charData, start = start, end = end))
} }

@ -34,14 +34,15 @@ data class TextPage(
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))) (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)))
textLine.lineBottom = textLine.lineBottom =
textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent
var x = (ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2 var x = ChapterProvider.paddingLeft +
(ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2
textLine.text = textLine.text =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
for (i in textLine.text.indices) { for (i in textLine.text.indices) {
val char = textLine.text[i].toString() val char = textLine.text[i].toString()
val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint) val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
val x1 = x + cw val x1 = x + cw
textLine.textChars.add(TextChar(charData = char, start = x, end = x1)) textLine.addTextChar(charData = char, start = x, end = x1)
x = x1 x = x1
} }
textLines.add(textLine) textLines.add(textLine)

@ -214,7 +214,8 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
customView { customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view editText = edit_view
edit_view.setFilterValues(cacheUrls) { edit_view.setFilterValues(cacheUrls)
edit_view.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)
aCache.put(importRecordKey, cacheUrls.joinToString(",")) aCache.put(importRecordKey, cacheUrls.joinToString(","))
} }

@ -3,7 +3,9 @@ package io.legado.app.ui.config
import android.os.Bundle import android.os.Bundle
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.activity_config.* import kotlinx.android.synthetic.main.activity_config.*
class ConfigActivity : VMBaseActivity<ConfigViewModel>(R.layout.activity_config) { class ConfigActivity : VMBaseActivity<ConfigViewModel>(R.layout.activity_config) {
@ -47,4 +49,10 @@ class ConfigActivity : VMBaseActivity<ConfigViewModel>(R.layout.activity_config)
} }
override fun observeLiveBus() {
super.observeLiveBus()
observeEvent<String>(EventBus.RECREATE) {
recreate()
}
}
} }

@ -18,6 +18,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.receiver.SharedReceiverActivity import io.legado.app.receiver.SharedReceiverActivity
import io.legado.app.service.WebService
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.number.NumberPickerDialog
@ -34,12 +35,14 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
App.INSTANCE, App.INSTANCE,
SharedReceiverActivity::class.java.name SharedReceiverActivity::class.java.name
) )
private val webPort get() = getPrefInt(PreferKey.webPort, 1122)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
addPreferencesFromResource(R.xml.pref_config_other) addPreferencesFromResource(R.xml.pref_config_other)
upPreferenceSummary(PreferKey.downloadPath, BookHelp.downloadPath) upPreferenceSummary(PreferKey.downloadPath, BookHelp.downloadPath)
upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString()) upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString())
upPreferenceSummary(PreferKey.webPort, webPort.toString())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -63,6 +66,14 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
.show { .show {
AppConfig.threadCount = it AppConfig.threadCount = it
} }
PreferKey.webPort -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.web_port_title))
.setMaxValue(60000)
.setMinValue(1024)
.setValue(webPort)
.show {
putPrefInt(PreferKey.webPort, it)
}
PreferKey.downloadPath -> selectDownloadPath() PreferKey.downloadPath -> selectDownloadPath()
PreferKey.cleanCache -> { PreferKey.cleanCache -> {
BookHelp.clearCache() BookHelp.clearCache()
@ -78,9 +89,15 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
upPreferenceSummary(key, BookHelp.downloadPath) upPreferenceSummary(key, BookHelp.downloadPath)
} }
PreferKey.threadCount -> upPreferenceSummary( PreferKey.threadCount -> upPreferenceSummary(
PreferKey.threadCount, key, AppConfig.threadCount.toString()
AppConfig.threadCount.toString()
) )
PreferKey.webPort -> {
upPreferenceSummary(key, webPort.toString())
if (WebService.isRun) {
WebService.stop(requireContext())
WebService.start(requireContext())
}
}
PreferKey.recordLog -> LogUtils.upLevel() PreferKey.recordLog -> LogUtils.upLevel()
PreferKey.processText -> sharedPreferences?.let { PreferKey.processText -> sharedPreferences?.let {
setProcessTextEnable(it.getBoolean(key, true)) setProcessTextEnable(it.getBoolean(key, true))
@ -93,6 +110,7 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
val preference = findPreference<Preference>(preferenceKey) ?: return val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) { when (preferenceKey) {
PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value) PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value)
PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value)
else -> if (preference is ListPreference) { else -> if (preference is ListPreference) {
val index = preference.findIndexOfValue(value) val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value. // Set the summary to reflect the new value.

@ -76,4 +76,8 @@ class SourceLogin : BaseActivity(R.layout.activity_source_login) {
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
override fun onDestroy() {
super.onDestroy()
web_view.destroy()
}
} }

@ -34,14 +34,18 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
get() = getViewModel(MainViewModel::class.java) get() = getViewModel(MainViewModel::class.java)
private var pagePosition = 0 private var pagePosition = 0
private val fragmentList = arrayListOf<Fragment>() private val fragmentId = arrayOf(0, 1, 2, 3)
private var rssFragment: RssFragment? = null private val fragmentMap = mapOf<Int, Fragment>(
Pair(fragmentId[0], BookshelfFragment()),
Pair(fragmentId[1], ExploreFragment()),
Pair(fragmentId[2], RssFragment()),
Pair(fragmentId[3], MyFragment())
)
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
ATH.applyEdgeEffectColor(view_pager_main) ATH.applyEdgeEffectColor(view_pager_main)
ATH.applyBottomNavigationColor(bottom_navigation_view) ATH.applyBottomNavigationColor(bottom_navigation_view)
view_pager_main.offscreenPageLimit = 3 view_pager_main.offscreenPageLimit = 3
upFragmentList()
view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager)
view_pager_main.addOnPageChangeListener(this) view_pager_main.addOnPageChangeListener(this)
bottom_navigation_view.setOnNavigationItemSelectedListener(this) bottom_navigation_view.setOnNavigationItemSelectedListener(this)
@ -69,22 +73,6 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
return false return false
} }
private fun upFragmentList() {
if (fragmentList.isEmpty()) {
fragmentList.add(BookshelfFragment())
fragmentList.add(ExploreFragment())
fragmentList.add(RssFragment())
fragmentList.add(MyFragment())
}
if (AppConfig.isShowRSS && fragmentList.size < 4) {
fragmentList.add(2,
rssFragment ?: RssFragment().apply { rssFragment = this })
}
if (!AppConfig.isShowRSS && fragmentList.size == 4) {
fragmentList.removeAt(2)
}
}
private fun upVersion() { private fun upVersion() {
if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) { if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) {
putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode) putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
@ -144,7 +132,6 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
observeEvent<String>(EventBus.SHOW_RSS) { observeEvent<String>(EventBus.SHOW_RSS) {
bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
upFragmentList()
view_pager_main.adapter?.notifyDataSetChanged() view_pager_main.adapter?.notifyDataSetChanged()
if (AppConfig.isShowRSS) { if (AppConfig.isShowRSS) {
view_pager_main.setCurrentItem(3, false) view_pager_main.setCurrentItem(3, false)
@ -160,7 +147,16 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
override fun getItem(position: Int): Fragment { override fun getItem(position: Int): Fragment {
return fragmentList[position] return when (position) {
0 -> fragmentMap.getValue(fragmentId[0])
1 -> fragmentMap.getValue(fragmentId[1])
2 -> if (AppConfig.isShowRSS) {
fragmentMap.getValue(fragmentId[2])
} else {
fragmentMap.getValue(fragmentId[3])
}
else -> fragmentMap.getValue(fragmentId[3])
}
} }
override fun getCount(): Int { override fun getCount(): Int {
@ -168,4 +164,5 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
} }
} }

@ -15,7 +15,6 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
@ -25,10 +24,10 @@ import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.arrange.ArrangeBookActivity import io.legado.app.ui.book.arrange.ArrangeBookActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.download.DownloadActivity import io.legado.app.ui.book.download.DownloadActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.* import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
@ -99,9 +98,6 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position -> TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position ->
tab.text = bookGroups[position].groupName tab.text = bookGroups[position].groupName
}.attach() }.attach()
observeEvent<Int>(EventBus.UP_TABS) {
tab_layout.getTabAt(it)?.select()
}
} }
private fun initBookGroupData() { private fun initBookGroupData() {

@ -57,9 +57,6 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
} }
initRecyclerView() initRecyclerView()
upRecyclerData() upRecyclerData()
observeEvent<String>(EventBus.UP_BOOK) {
booksAdapter.notification(it)
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -135,4 +132,10 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
return bookUrl in activityViewModel.updateList return bookUrl in activityViewModel.updateList
} }
override fun observeLiveBus() {
super.observeLiveBus()
observeEvent<String>(EventBus.UP_BOOK) {
booksAdapter.notification(it)
}
}
} }

@ -23,6 +23,7 @@ import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.ui.widget.prefs.NameListPreference
import io.legado.app.ui.widget.prefs.SwitchPreference import io.legado.app.ui.widget.prefs.SwitchPreference
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
@ -74,6 +75,12 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.
observeEvent<Boolean>(EventBus.WEB_SERVICE_STOP) { observeEvent<Boolean>(EventBus.WEB_SERVICE_STOP) {
webServicePre?.isChecked = false webServicePre?.isChecked = false
} }
findPreference<NameListPreference>(PreferKey.themeMode)?.let {
it.setOnPreferenceChangeListener { _, _ ->
view?.post { App.INSTANCE.applyDayNight() }
true
}
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -96,7 +103,6 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.
key: String? key: String?
) { ) {
when (key) { when (key) {
PreferKey.themeMode -> App.INSTANCE.applyDayNight()
PreferKey.webService -> { PreferKey.webService -> {
if (requireContext().getPrefBoolean("webService")) { if (requireContext().getPrefBoolean("webService")) {
WebService.start(requireContext()) WebService.start(requireContext())

@ -213,7 +213,8 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
customView { customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view editText = edit_view
edit_view.setFilterValues(cacheUrls) { edit_view.setFilterValues(cacheUrls)
edit_view.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)
aCache.put(importRecordKey, cacheUrls.joinToString(",")) aCache.put(importRecordKey, cacheUrls.joinToString(","))
} }

@ -16,6 +16,7 @@ import io.legado.app.constant.Theme
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.toast
import kotlinx.android.synthetic.main.dialog_replace_edit.* import kotlinx.android.synthetic.main.dialog_replace_edit.*
class ReplaceEditDialog : DialogFragment(), class ReplaceEditDialog : DialogFragment(),
@ -73,12 +74,18 @@ class ReplaceEditDialog : DialogFragment(),
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_save -> { R.id.menu_save -> {
viewModel.save(getReplaceRule()) { val rule = getReplaceRule();
if (!rule.isValid()){
toast(R.string.replace_rule_invalid)
}
else{
viewModel.save(rule) {
callBack?.onReplaceRuleSave() callBack?.onReplaceRuleSave()
dismiss() dismiss()
} }
} }
} }
}
return true return true
} }

@ -203,4 +203,9 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
} }
} }
override fun onDestroy() {
super.onDestroy()
web_view.destroy()
}
} }

@ -227,7 +227,8 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
customView { customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view editText = edit_view
edit_view.setFilterValues(cacheUrls) { edit_view.setFilterValues(cacheUrls)
edit_view.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)
aCache.put(importRecordKey, cacheUrls.joinToString(",")) aCache.put(importRecordKey, cacheUrls.joinToString(","))
} }

@ -0,0 +1,79 @@
package io.legado.app.ui.widget
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import io.legado.app.R
class ArcView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var mWidth = 0
private var mHeight = 0
//弧形高度
private val mArcHeight: Int
//背景颜色
private val mBgColor: Int
private val mPaint: Paint = Paint()
private val mDirectionTop: Boolean
val rect = Rect()
val path = Path()
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcView)
mArcHeight = typedArray.getDimensionPixelSize(R.styleable.ArcView_arcHeight, 0)
mBgColor = typedArray.getColor(
R.styleable.ArcView_bgColor,
Color.parseColor("#303F9F")
)
mDirectionTop = typedArray.getBoolean(R.styleable.ArcView_arcDirectionTop, false)
typedArray.recycle()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mPaint.style = Paint.Style.FILL
mPaint.color = mBgColor
if (mDirectionTop) {
rect.set(0, mArcHeight, mWidth, mHeight)
canvas.drawRect(rect, mPaint)
path.reset()
path.moveTo(0f, mArcHeight.toFloat())
path.quadTo(mWidth / 2.toFloat(), 0f, mWidth.toFloat(), mArcHeight.toFloat())
canvas.drawPath(path, mPaint)
} else {
rect.set(0, 0, mWidth, mHeight - mArcHeight)
canvas.drawRect(rect, mPaint)
path.reset()
path.moveTo(0f, mHeight - mArcHeight.toFloat())
path.quadTo(
mWidth / 2.toFloat(),
mHeight.toFloat(),
mWidth.toFloat(),
mHeight - mArcHeight.toFloat()
)
canvas.drawPath(path, mPaint)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize
}
setMeasuredDimension(mWidth, mHeight)
}
}

@ -60,8 +60,7 @@ class FontSelectDialog : BaseDialogFragment(),
return inflater.inflate(R.layout.dialog_font_select, container) return inflater.inflate(R.layout.dialog_font_select, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tool_bar.setTitle(R.string.select_font) tool_bar.setTitle(R.string.select_font)
tool_bar.inflateMenu(R.menu.font_select) tool_bar.inflateMenu(R.menu.font_select)
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)

@ -42,15 +42,13 @@ class AutoCompleteTextView : AppCompatAutoCompleteTextView {
return super.onTouchEvent(event) return super.onTouchEvent(event)
} }
fun setFilterValues(values: List<String>?, delCallBack: ((value: String) -> Unit)? = null) { fun setFilterValues(values: List<String>?) {
this.delCallBack = delCallBack
values?.let { values?.let {
setAdapter(MyAdapter(context, values)) setAdapter(MyAdapter(context, values))
} }
} }
fun setFilterValues(vararg value: String, delCallBack: ((value: String) -> Unit)? = null) { fun setFilterValues(vararg value: String) {
this.delCallBack = delCallBack
setAdapter(MyAdapter(context, value.toMutableList())) setAdapter(MyAdapter(context, value.toMutableList()))
} }

@ -0,0 +1,81 @@
package io.legado.app.ui.widget.text
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatTextView
class ScrollTextView(context: Context?, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
//滑动距离的最大边界
private var mOffsetHeight = 0
//是否到顶或者到底的标志
private var mBottomFlag = false
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
initOffsetHeight()
}
override fun onTextChanged(
text: CharSequence,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
initOffsetHeight()
}
private fun initOffsetHeight() {
val mLayoutHeight: Int
//获得内容面板
val mLayout = layout ?: return
//获得内容面板的高度
mLayoutHeight = mLayout.height
//获取上内边距
val paddingTop: Int = totalPaddingTop
//获取下内边距
val paddingBottom: Int = totalPaddingBottom
//获得控件的实际高度
val mHeight: Int = measuredHeight
//计算滑动距离的边界
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
if (mOffsetHeight <= 0) {
scrollTo(0, 0)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
//如果是新的按下事件,则对mBottomFlag重新初始化
mBottomFlag = mOffsetHeight <= 0
}
//如果已经不要这次事件,则传出取消的信号,这里的作用不大
if (mBottomFlag) {
event.action = MotionEvent.ACTION_CANCEL
}
return super.dispatchTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
val result = super.onTouchEvent(event)
//如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次
if (!mBottomFlag) parent.requestDisallowInterceptTouchEvent(true)
return result
}
override fun onScrollChanged(horiz: Int, vert: Int, oldHoriz: Int, oldVert: Int) {
super.onScrollChanged(horiz, vert, oldHoriz, oldVert)
if (vert == mOffsetHeight || vert == 0) {
//这里触发父布局或祖父布局的滑动事件
parent.requestDisallowInterceptTouchEvent(false)
mBottomFlag = true
}
}
}

@ -4,13 +4,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import com.jeremyliao.liveeventbus.core.Observable
inline fun <reified EVENT> eventObservable(tag: String): LiveEventBus.Observable<EVENT> { inline fun <reified EVENT> eventObservable(tag: String): Observable<EVENT> {
return LiveEventBus.get().with(tag, EVENT::class.java) return LiveEventBus.get(tag, EVENT::class.java)
} }
inline fun <reified EVENT> postEvent(tag: String, event: EVENT) { inline fun <reified EVENT> postEvent(tag: String, event: EVENT) {
LiveEventBus.get().with(tag, EVENT::class.java).post(event) LiveEventBus.get(tag).post(event)
} }
inline fun <reified EVENT> AppCompatActivity.observeEvent( inline fun <reified EVENT> AppCompatActivity.observeEvent(

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="16"
android:viewportWidth="16"
android:width="24dp">
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,2h15C15.8,2 16,2.2 16,2.5l0,0C16,2.8 15.8,3 15.5,3h-15C0.2,3 0,2.8 0,2.5l0,0C0,2.2 0.2,2 0.5,2z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,13h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,14 0,13.8 0,13.5l0,0C0,13.2 0.2,13 0.5,13z" />
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="16"
android:viewportWidth="16"
android:width="24dp">
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,2h15C15.8,2 16,2.2 16,2.5l0,0C16,2.8 15.8,3 15.5,3h-15C0.2,3 0,2.8 0,2.5l0,0C0,2.2 0.2,2 0.5,2z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,7.4h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,8.4 0,8.2 0,7.9l0,0C0,7.7 0.2,7.4 0.5,7.4z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,13h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,14 0,13.8 0,13.5l0,0C0,13.2 0.2,13 0.5,13z" />
</vector>

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="16"
android:viewportWidth="16"
android:width="24dp">
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,2h15C15.8,2 16,2.2 16,2.5l0,0C16,2.8 15.8,3 15.5,3h-15C0.2,3 0,2.8 0,2.5l0,0C0,2.2 0.2,2 0.5,2z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,5.7h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,6.7 0,6.5 0,6.2l0,0C0,5.9 0.2,5.7 0.5,5.7z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,9.3h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,10.3 0,10.1 0,9.8l0,0C0,9.5 0.2,9.3 0.5,9.3z" />
<path
android:fillColor="#3C3F43"
android:pathData="M0.5,13h15c0.3,0 0.5,0.2 0.5,0.5l0,0c0,0.3 -0.2,0.5 -0.5,0.5h-15C0.2,14 0,13.8 0,13.5l0,0C0,13.2 0.2,13 0.5,13z" />
</vector>

@ -9,19 +9,17 @@
<ImageView <ImageView
android:id="@+id/bg_book" android:id="@+id/bg_book"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:contentDescription="@string/bg_image" android:contentDescription="@string/bg_image"
app:layout_constraintBottom_toTopOf="@+id/view_info"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<View <LinearLayout
android:id="@+id/vw_bg" android:id="@+id/vw_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:background="#50000000" android:background="#50000000"
app:layout_constraintBottom_toTopOf="@+id/view_info" android:orientation="vertical">
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
@ -33,61 +31,167 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:title="@string/book_info" /> app:title="@string/book_info" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<io.legado.app.ui.widget.ArcView
app:arcHeight="36dp"
app:bgColor="@color/background"
app:arcDirectionTop="true"
android:layout_marginTop="90dp"
android:layout_width="match_parent"
android:layout_height="78dp" />
<androidx.cardview.widget.CardView
app:cardCornerRadius="5dp"
app:cardElevation="8dp"
android:layout_margin="3dp"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<io.legado.app.ui.widget.image.CoverImageView <io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover" android:id="@+id/iv_cover"
android:layout_width="110dp" android:layout_width="110dp"
android:layout_height="160dp" android:layout_height="160dp"
android:layout_margin="10dp" android:contentDescription="@string/img_cover"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> </androidx.cardview.widget.CardView>
</RelativeLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_book_info" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="12dp" android:background="@color/background"
android:layout_marginRight="12dp"
android:minHeight="115dp"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@+id/iv_cover" android:paddingLeft="10dp"
app:layout_constraintLeft_toRightOf="@+id/iv_cover" android:paddingTop="8dp"
app:layout_constraintRight_toRightOf="parent" android:paddingRight="10dp"
app:layout_constraintTop_toTopOf="@+id/iv_cover"> android:paddingBottom="3dp">
<TextView <TextView
android:id="@+id/tv_name" android:id="@+id/tv_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="6dp" android:layout_marginBottom="6dp"
android:gravity="center"
android:includeFontPadding="false"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/md_white_1000" android:text="@string/book_name"
android:textSize="18sp" /> android:textColor="@color/tv_text_default"
android:textSize="18sp"
tools:ignore="RtlHardcoded" />
<TextView <io.legado.app.ui.widget.LabelsBar
android:id="@+id/tv_author" android:id="@+id/lb_kind"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="3dp" android:layout_marginBottom="3dp"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone" />
</LinearLayout>
<ScrollView
android:background="@color/background"
android:layout_weight="1.0"
android:padding="0dp"
android:layout_height="0dp"
android:layout_width="match_parent"
android:scrollbarStyle="outsideInset"
android:scrollbars="vertical"
android:fillViewport="true"
android:fitsSystemWindows="false"
android:focusable="true" >
<LinearLayout
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:background="@color/background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:background="@color/background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="3dp"
android:paddingBottom="3dp">
<ImageView
android:layout_width="18sp"
android:layout_height="18sp"
android:contentDescription="@string/origin_format"
android:paddingRight="2dp"
android:src="@drawable/ic_author"
app:tint="@color/tv_text_summary"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<TextView
android:id="@+id/tv_author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:paddingRight="6dp"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/md_white_1000" android:text="@string/author"
android:textSize="16sp" /> android:textColor="@color/tv_text_summary"
android:textSize="13sp"
tools:ignore="NestedWeights" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:paddingTop="3dp"
android:paddingBottom="3dp">
<ImageView
android:id="@+id/iv_web"
android:layout_width="18sp"
android:layout_height="18sp"
android:contentDescription="@string/origin_format"
android:paddingRight="2dp"
android:src="@drawable/ic_web_outline"
app:tint="@color/tv_text_summary"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<TextView <TextView
android:id="@+id/tv_origin" android:id="@+id/tv_origin"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:paddingRight="6dp"
android:singleLine="true" android:singleLine="true"
android:textSize="16sp" android:text="@string/origin_format"
android:textColor="@color/md_white_1000" android:textColor="@color/tv_text_summary"
android:textSize="13sp"
tools:ignore="NestedWeights" /> tools:ignore="NestedWeights" />
<io.legado.app.ui.widget.text.AccentBgTextView <io.legado.app.ui.widget.text.AccentBgTextView
@ -103,71 +207,64 @@
</LinearLayout> </LinearLayout>
<io.legado.app.ui.widget.LabelsBar <LinearLayout
android:id="@+id/lb_kind"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="3dp" android:gravity="center_vertical"
android:visibility="gone" /> android:orientation="horizontal"
android:paddingTop="3dp"
android:paddingBottom="3dp">
<ImageView
android:id="@+id/ic_book_last"
android:layout_width="18sp"
android:layout_height="18sp"
android:contentDescription="@string/read_dur_progress"
android:paddingRight="2dp"
android:src="@drawable/ic_book_last"
app:tint="@color/tv_text_summary"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<TextView <TextView
android:id="@+id/tv_lasted" android:id="@+id/tv_lasted"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:paddingRight="6dp"
android:singleLine="true" android:singleLine="true"
android:textSize="16sp" android:text="@string/read_dur_progress"
android:textColor="@color/md_white_1000" android:textColor="@color/tv_text_summary"
app:layout_constraintBottom_toBottomOf="@+id/iv_cover" android:textSize="13sp"
app:layout_constraintLeft_toLeftOf="@+id/tv_name" tools:ignore="NestedWeights" />
app:layout_constraintRight_toRightOf="@id/tv_name" />
</LinearLayout> </LinearLayout>
<LinearLayout </LinearLayout>
android:id="@+id/view_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/view_other"
app:layout_constraintTop_toBottomOf="@+id/iv_cover">
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@color/bg_divider_line" />
<TextView <io.legado.app.ui.widget.text.ScrollTextView
android:id="@+id/tv_intro"
android:layout_marginTop="8dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp" android:clickable="true"
android:focusable="true"
android:text="@string/book_intro" android:text="@string/book_intro"
android:textStyle="bold" android:textColor="@color/tv_text_secondary"
android:textColor="@color/tv_text_default" android:textSize="14sp"
android:textSize="16sp" /> android:paddingLeft="8dp"
android:visibility="visible" />
<TextView
android:id="@+id/tv_intro"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:minHeight="100dp"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@color/bg_divider_line" />
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout <LinearLayout
android:id="@+id/view_other" android:id="@+id/view_other"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/background"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
@ -180,6 +277,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="12dp" android:padding="12dp"
android:singleLine="true" android:singleLine="true"
android:clickable="true"
android:text="@string/group_s" android:text="@string/group_s"
android:textColor="@color/tv_text_default" android:textColor="@color/tv_text_default"
android:textSize="16sp" /> android:textSize="16sp" />
@ -189,13 +287,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="12dp" android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:singleLine="true" android:singleLine="true"
android:clickable="true"
android:text="@string/toc_s" android:text="@string/toc_s"
android:textColor="@color/tv_text_default" android:textColor="@color/tv_text_default"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/bg_divider_line" />
<LinearLayout <LinearLayout
android:id="@+id/fl_action" android:id="@+id/fl_action"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -232,4 +337,7 @@
</LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -5,11 +5,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_menu">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar" android:id="@+id/tool_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -19,13 +14,6 @@
app:displayHomeAsUp="false" app:displayHomeAsUp="false"
app:fitStatusBar="false" /> app:fitStatusBar="false" />
<androidx.appcompat.widget.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<io.legado.app.ui.widget.anima.RefreshProgressBar <io.legado.app.ui.widget.anima.RefreshProgressBar
android:id="@+id/refresh_progress_bar" android:id="@+id/refresh_progress_bar"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_view" android:id="@+id/root_view"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -9,19 +8,33 @@
android:background="@color/background" android:background="@color/background"
android:padding="10dp"> android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5" />
<io.legado.app.ui.widget.text.StrokeTextView <io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_title_center" android:id="@+id/tv_title_mode"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:paddingTop="3dp" android:paddingTop="4dp"
android:paddingBottom="3dp" android:paddingBottom="4dp"
android:text="@string/title_center" android:text="@string/title_center"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" /> <Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.text.StrokeTextView <io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_text_bold" android:id="@+id/tv_text_bold"
@ -29,14 +42,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:paddingTop="3dp" android:paddingTop="4dp"
android:paddingBottom="3dp" android:paddingBottom="4dp"
android:text="@string/text_bold" android:text="@string/text_bold"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp" />
app:layout_constraintLeft_toRightOf="@+id/tv_title_center"
app:layout_constraintRight_toLeftOf="@+id/tv_text_font" <Space
app:layout_constraintTop_toTopOf="parent" /> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.text.StrokeTextView <io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_text_font" android:id="@+id/tv_text_font"
@ -44,14 +59,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:paddingTop="3dp" android:paddingTop="4dp"
android:paddingBottom="3dp" android:paddingBottom="4dp"
android:text="@string/text_font" android:text="@string/text_font"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp" />
app:layout_constraintLeft_toRightOf="@+id/tv_text_bold"
app:layout_constraintRight_toLeftOf="@+id/tv_text_indent" <Space
app:layout_constraintTop_toTopOf="parent" /> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.text.StrokeTextView <io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_text_indent" android:id="@+id/tv_text_indent"
@ -59,29 +76,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:paddingTop="3dp" android:paddingTop="4dp"
android:paddingBottom="3dp" android:paddingBottom="4dp"
android:text="@string/text_indent" android:text="@string/text_indent"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp" />
app:layout_constraintLeft_toRightOf="@+id/tv_text_font"
app:layout_constraintRight_toLeftOf="@+id/tv_padding"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.text.StrokeTextView <Space
android:id="@+id/tv_padding" android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:layout_weight="1" />
android:paddingRight="6dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:text="@string/padding"
android:gravity="center"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@+id/tv_text_indent"
app:layout_constraintRight_toLeftOf="@id/chinese_converter"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.book.read.config.ChineseConverter <io.legado.app.ui.book.read.config.ChineseConverter
android:id="@+id/chinese_converter" android:id="@+id/chinese_converter"
@ -89,60 +93,116 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:paddingTop="3dp" android:paddingTop="4dp"
android:paddingBottom="3dp" android:paddingBottom="4dp"
android:gravity="center" android:gravity="center"
android:textSize="14sp" android:textSize="14sp" />
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.DetailSeekBar <Space
android:id="@+id/dsb_text_size" android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_weight="0.5" />
android:paddingLeft="10dp"
android:paddingRight="10dp" </LinearLayout>
app:max="45"
app:title="@string/text_size"
app:layout_constraintTop_toBottomOf="@+id/tv_text_bold" />
<io.legado.app.ui.widget.DetailSeekBar <LinearLayout
android:id="@+id/dsb_text_letter_spacing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:paddingTop="6dp"
android:paddingRight="10dp" android:paddingBottom="6dp"
app:max="100" android:gravity="center_vertical">
app:title="@string/text_letter_spacing"
app:layout_constraintTop_toBottomOf="@+id/dsb_text_size" />
<io.legado.app.ui.widget.DetailSeekBar <Space
android:id="@+id/dsb_line_size" android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:layout_weight="0.5" />
android:paddingRight="10dp"
app:max="50"
app:title="@string/line_size"
app:layout_constraintTop_toBottomOf="@+id/dsb_text_letter_spacing" />
<io.legado.app.ui.widget.DetailSeekBar <ImageView
android:id="@+id/dsb_paragraph_spacing" android:id="@+id/iv_default1"
android:layout_width="match_parent" android:layout_width="30dp"
android:layout_height="30dp"
android:background="?attr/selectableItemBackground"
android:tint="@color/tv_text_default"
android:padding="6dp"
android:contentDescription="@string/default1"
android:src="@drawable/ic_type_default_1" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:layout_weight="1" />
android:paddingRight="10dp"
app:max="50" <ImageView
app:title="@string/paragraph_size" android:id="@+id/iv_default2"
app:layout_constraintTop_toBottomOf="@+id/dsb_line_size" /> android:layout_width="30dp"
android:layout_height="30dp"
android:background="?attr/selectableItemBackground"
android:tint="@color/tv_text_default"
android:padding="6dp"
android:contentDescription="@string/default2"
android:src="@drawable/ic_type_default_2" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageView
android:id="@+id/iv_default3"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="?attr/selectableItemBackground"
android:tint="@color/tv_text_default"
android:padding="6dp"
android:contentDescription="@string/default3"
android:src="@drawable/ic_type_default_3" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/compose_type"
android:gravity="center"
android:textSize="14sp" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.text.StrokeTextView
android:id="@+id/tv_padding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/padding"
android:gravity="center"
android:textSize="14sp" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5" />
</LinearLayout>
<View <View
android:id="@+id/vw_bg_fg" android:id="@+id/vw_bg_fg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0.8dp" android:layout_height="0.8dp"
android:background="@color/btn_bg_press" android:background="@color/btn_bg_press" />
app:layout_constraintTop_toBottomOf="@+id/dsb_paragraph_spacing" />
<TextView <TextView
android:id="@+id/tv_page_anim" android:id="@+id/tv_page_anim"
@ -150,8 +210,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="6dp" android:paddingStart="6dp"
android:paddingEnd="6dp" android:paddingEnd="6dp"
android:text="@string/page_anim" android:text="@string/page_anim" />
app:layout_constraintTop_toBottomOf="@+id/vw_bg_fg" />
<RadioGroup <RadioGroup
android:id="@+id/rg_page_anim" android:id="@+id/rg_page_anim"
@ -160,8 +219,7 @@
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="10dp" android:paddingRight="10dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center" android:gravity="center">
app:layout_constraintTop_toBottomOf="@id/tv_page_anim">
<io.legado.app.lib.theme.view.ATERadioNoButton <io.legado.app.lib.theme.view.ATERadioNoButton
android:id="@+id/rb_anim0" android:id="@+id/rb_anim0"
@ -229,42 +287,50 @@
android:id="@+id/vw_bg_fg1" android:id="@+id/vw_bg_fg1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0.8dp" android:layout_height="0.8dp"
android:background="@color/btn_bg_press" android:background="@color/btn_bg_press" />
app:layout_constraintTop_toBottomOf="@+id/rg_page_anim" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView <TextView
android:id="@+id/tv_bg_ts" android:id="@+id/tv_bg_ts"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="6dp" android:paddingStart="6dp"
android:paddingEnd="6dp" android:paddingEnd="6dp"
android:paddingTop="5dp" android:paddingTop="5dp"
android:paddingBottom="5dp" android:paddingBottom="5dp"
android:text="@string/text_bg_style" android:text="@string/text_bg_style" />
app:layout_constraintTop_toBottomOf="@+id/vw_bg_fg1"
app:layout_constraintLeft_toLeftOf="parent" <TextView
app:layout_constraintRight_toLeftOf="@+id/tv_share_layout" /> android:id="@+id/tv_share_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/share_layout" />
<io.legado.app.ui.widget.checkbox.SmoothCheckBox <io.legado.app.ui.widget.checkbox.SmoothCheckBox
android:id="@+id/cb_share_layout" android:id="@+id/cb_share_layout"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp" android:layout_height="20dp"
android:layout_marginRight="6dp" android:layout_marginRight="6dp"
app:layout_constraintTop_toTopOf="@id/tv_bg_ts" android:layout_marginLeft="6dp" />
app:layout_constraintBottom_toBottomOf="@id/tv_bg_ts"
app:layout_constraintRight_toRightOf="parent"
tools:ignore="RtlHardcoded" />
<TextView </LinearLayout>
android:id="@+id/tv_share_layout"
android:layout_width="wrap_content" <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="3dp" android:orientation="horizontal"
android:paddingRight="3dp" android:gravity="center_vertical">
android:text="@string/share_layout"
app:layout_constraintTop_toTopOf="@+id/tv_bg_ts" <Space
app:layout_constraintBottom_toBottomOf="@+id/tv_bg_ts" android:layout_width="0dp"
app:layout_constraintRight_toLeftOf="@+id/cb_share_layout" /> android:layout_height="wrap_content"
android:layout_weight="0.5" />
<io.legado.app.ui.widget.image.CircleImageView <io.legado.app.ui.widget.image.CircleImageView
android:id="@+id/bg0" android:id="@+id/bg0"
@ -273,11 +339,12 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/bg1" <Space
app:layout_constraintBottom_toBottomOf="parent" android:layout_width="0dp"
app:layout_constraintTop_toBottomOf="@+id/tv_bg_ts" /> android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.image.CircleImageView <io.legado.app.ui.widget.image.CircleImageView
android:id="@+id/bg1" android:id="@+id/bg1"
@ -286,11 +353,12 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text" />
app:layout_constraintLeft_toRightOf="@+id/bg0"
app:layout_constraintRight_toLeftOf="@+id/bg2" <Space
app:layout_constraintBottom_toBottomOf="parent" android:layout_width="0dp"
app:layout_constraintTop_toTopOf="@+id/bg0" /> android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.image.CircleImageView <io.legado.app.ui.widget.image.CircleImageView
android:id="@+id/bg2" android:id="@+id/bg2"
@ -299,11 +367,12 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text" />
app:layout_constraintLeft_toRightOf="@+id/bg1"
app:layout_constraintRight_toLeftOf="@+id/bg3" <Space
app:layout_constraintBottom_toBottomOf="parent" android:layout_width="0dp"
app:layout_constraintTop_toTopOf="@+id/bg0" /> android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.image.CircleImageView <io.legado.app.ui.widget.image.CircleImageView
android:id="@+id/bg3" android:id="@+id/bg3"
@ -312,11 +381,12 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text" />
app:layout_constraintLeft_toRightOf="@+id/bg2"
app:layout_constraintRight_toLeftOf="@+id/bg4" <Space
app:layout_constraintBottom_toBottomOf="parent" android:layout_width="0dp"
app:layout_constraintTop_toTopOf="@+id/bg0" /> android:layout_height="wrap_content"
android:layout_weight="1" />
<io.legado.app.ui.widget.image.CircleImageView <io.legado.app.ui.widget.image.CircleImageView
android:id="@+id/bg4" android:id="@+id/bg4"
@ -325,10 +395,13 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text" />
app:layout_constraintLeft_toRightOf="@+id/bg3"
app:layout_constraintRight_toRightOf="parent" <Space
app:layout_constraintBottom_toBottomOf="parent" android:layout_width="0dp"
app:layout_constraintTop_toTopOf="@+id/bg0" /> android:layout_height="wrap_content"
android:layout_weight="0.5" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>

@ -7,20 +7,24 @@
android:padding="10dp" android:padding="10dp"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_header_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<io.legado.app.ui.widget.text.AccentTextView <io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_header_padding" android:id="@+id/tv_header_padding"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="18sp" android:textSize="18sp"
android:visibility="gone"
android:text="@string/header" /> android:text="@string/header" />
<io.legado.app.ui.widget.DetailSeekBar <io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_header_padding_top" android:id="@+id/dsb_header_padding_top"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:title="@string/padding_top" app:title="@string/padding_top"
app:max="100" /> app:max="100" />
@ -28,7 +32,6 @@
android:id="@+id/dsb_header_padding_bottom" android:id="@+id/dsb_header_padding_bottom"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:title="@string/padding_bottom" app:title="@string/padding_bottom"
app:max="100" /> app:max="100" />
@ -36,7 +39,6 @@
android:id="@+id/dsb_header_padding_left" android:id="@+id/dsb_header_padding_left"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:title="@string/padding_left" app:title="@string/padding_left"
app:max="100" /> app:max="100" />
@ -44,14 +46,16 @@
android:id="@+id/dsb_header_padding_right" android:id="@+id/dsb_header_padding_right"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:title="@string/padding_right" app:title="@string/padding_right"
app:max="100" /> app:max="100" />
</LinearLayout>
<io.legado.app.ui.widget.text.AccentTextView <io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_body_padding" android:id="@+id/tv_body_padding"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="18sp" android:textSize="18sp"
android:text="@string/main_body" /> android:text="@string/main_body" />
@ -84,14 +88,34 @@
app:title="@string/padding_right" app:title="@string/padding_right"
app:max="100" /> app:max="100" />
<io.legado.app.ui.widget.text.AccentTextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<io.legado.app.ui.widget.text.AccentTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="10dp" android:paddingTop="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="18sp" android:textSize="18sp"
android:text="@string/footer" /> android:text="@string/footer" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/showLine" />
<io.legado.app.ui.widget.checkbox.SmoothCheckBox
android:id="@+id/cb_show_line"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="6dp" />
</LinearLayout>
<io.legado.app.ui.widget.DetailSeekBar <io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_footer_padding_top" android:id="@+id/dsb_footer_padding_top"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_text_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:max="45"
app:title="@string/text_size" />
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_text_letter_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:max="100"
app:title="@string/text_letter_spacing" />
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_line_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:max="20"
app:title="@string/line_size" />
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_paragraph_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:max="20"
app:title="@string/paragraph_size" />
</LinearLayout>

@ -43,6 +43,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="@id/iv_cover" app:layout_constraintBottom_toBottomOf="@id/iv_cover"
app:layout_constraintLeft_toLeftOf="@+id/tv_name" app:layout_constraintLeft_toLeftOf="@+id/tv_name"
app:layout_constraintRight_toRightOf="@id/tv_name" app:layout_constraintRight_toRightOf="@id/tv_name"

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_stop"
android:title="@string/stop"
android:icon="@drawable/ic_stop_black_24dp"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
</menu>

@ -3,6 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_screen"
android:title="@string/screen"
android:icon="@drawable/ic_screen"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item <item
android:id="@+id/menu_stop" android:id="@+id/menu_stop"
android:title="@string/stop" android:title="@string/stop"

@ -15,4 +15,9 @@
android:title="@string/import_default_rule" android:title="@string/import_default_rule"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_import"
android:title="@string/import_replace_rule_on_line"
app:showAsAction="never" />
</menu> </menu>

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="icons">
<item>ic_launcher</item>
<item>launcher1</item>
<item>launcher2</item>
<item>launcher3</item>
</string-array>
<string-array name="screen_time_out_value">
<item>0</item>
<item>60</item>
<item>120</item>
<item>180</item>
<item>-1</item>
</string-array>
<string-array name="theme_mode_v">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="tts_speech_per_value">
<item>0</item>
<item>1</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>103</item>
<item>106</item>
<item>110</item>
<item>111</item>
<item>11</item>
<item>6</item>
<item>9</item>
</string-array>
</resources>

@ -20,21 +20,6 @@
<item>百度主持</item> <item>百度主持</item>
</string-array> </string-array>
<string-array name="tts_speech_per_value">
<item>0</item>
<item>1</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>103</item>
<item>106</item>
<item>110</item>
<item>111</item>
<item>11</item>
<item>6</item>
<item>9</item>
</string-array>
<string-array name="indent"> <string-array name="indent">
<item>@string/indent_0</item> <item>@string/indent_0</item>
<item>@string/indent_1</item> <item>@string/indent_1</item>
@ -61,12 +46,6 @@
<item>暗色主题</item> <item>暗色主题</item>
</string-array> </string-array>
<string-array name="theme_mode_v">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="NavBarColors"> <string-array name="NavBarColors">
<item>自动</item> <item>自动</item>
<item>黑色</item> <item>黑色</item>
@ -82,14 +61,6 @@
<item>常亮</item> <item>常亮</item>
</string-array> </string-array>
<string-array name="screen_time_out_value">
<item>0</item>
<item>60</item>
<item>120</item>
<item>180</item>
<item>-1</item>
</string-array>
<string-array name="select_folder"> <string-array name="select_folder">
<item>@string/default_path</item> <item>@string/default_path</item>
<item>@string/sys_folder_picker</item> <item>@string/sys_folder_picker</item>
@ -103,15 +74,6 @@
<item>@string/screen_sensor</item> <item>@string/screen_sensor</item>
</string-array> </string-array>
<declare-styleable name="Battery">
<attr name="batteryOrientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="batteryColor" format="color" />
<attr name="batteryPower" format="integer" />
</declare-styleable>
<string-array name="icon_names"> <string-array name="icon_names">
<item>iconMain</item> <item>iconMain</item>
<item>icon1</item> <item>icon1</item>
@ -119,13 +81,6 @@
<item>icon3</item> <item>icon3</item>
</string-array> </string-array>
<string-array name="icons">
<item>ic_launcher</item>
<item>launcher1</item>
<item>launcher2</item>
<item>launcher3</item>
</string-array>
<string-array name="chinese_mode"> <string-array name="chinese_mode">
<item>关闭</item> <item>关闭</item>
<item>繁体转简体</item> <item>繁体转简体</item>
@ -137,4 +92,11 @@
<item>系统衬线字体</item> <item>系统衬线字体</item>
<item>系统等宽字体</item> <item>系统等宽字体</item>
</string-array> </string-array>
<string-array name="title_mode">
<item>标题靠左</item>
<item>标题居中</item>
<item>标题隐藏</item>
</string-array>
</resources> </resources>

@ -137,6 +137,15 @@
<attr name="textColor" format="color" /> <attr name="textColor" format="color" />
</declare-styleable> </declare-styleable>
<declare-styleable name="Battery">
<attr name="batteryOrientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="batteryColor" format="color" />
<attr name="batteryPower" format="integer" />
</declare-styleable>
<declare-styleable name="VerticalSeekBar"> <declare-styleable name="VerticalSeekBar">
<attr name="seekBarRotation"> <attr name="seekBarRotation">
<!-- Clock wise - 90 deg; top = min, bottom = max --> <!-- Clock wise - 90 deg; top = min, bottom = max -->
@ -154,4 +163,10 @@
<attr name="radius" /> <attr name="radius" />
<attr name="up_flat_angle" format="boolean" /> <attr name="up_flat_angle" format="boolean" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ArcView">
<attr name="arcHeight" format="dimension" />
<attr name="bgColor" format="color" />
<attr name="arcDirectionTop" format="boolean" />
</declare-styleable>
</resources> </resources>

@ -204,6 +204,7 @@
<string name="clear_all_content">清除缓存会删除所有已保存章节,是否确认删除?</string> <string name="clear_all_content">清除缓存会删除所有已保存章节,是否确认删除?</string>
<string name="book_source_share_url">书源共享</string> <string name="book_source_share_url">书源共享</string>
<string name="replace_rule_summary">替换规则名称</string> <string name="replace_rule_summary">替换规则名称</string>
<string name="replace_rule_invalid">替换规则为空或者不满足正则表达式要求</string>
<string name="select_action">选择操作</string> <string name="select_action">选择操作</string>
<string name="select_all">全选</string> <string name="select_all">全选</string>
<string name="select_all_count">全选(%d/%d)</string> <string name="select_all_count">全选(%d/%d)</string>
@ -226,13 +227,14 @@
<string name="open_from_other">打开外部书籍</string> <string name="open_from_other">打开外部书籍</string>
<string name="origin_show">来源:%s</string> <string name="origin_show">来源:%s</string>
<string name="import_replace_rule">本地导入</string> <string name="import_replace_rule">本地导入</string>
<string name="import_replace_rule_on_line">网络导入</string> <string name="import_replace_rule_on_line">导入在线规则</string>
<string name="check_update_interval">检查更新间隔</string> <string name="check_update_interval">检查更新间隔</string>
<string name="bookshelf_px_0">按阅读时间</string> <string name="bookshelf_px_0">按阅读时间</string>
<string name="bookshelf_px_1">按更新时间</string> <string name="bookshelf_px_1">按更新时间</string>
<string name="bookshelf_px_2">按书名</string> <string name="bookshelf_px_2">按书名</string>
<string name="bookshelf_px_3">手动排序</string> <string name="bookshelf_px_3">手动排序</string>
<string name="read_type">阅读方式</string> <string name="read_type">阅读方式</string>
<string name="compose_type">排版</string>
<string name="del_select">删除所选</string> <string name="del_select">删除所选</string>
<string name="del_msg">是否确认删除?</string> <string name="del_msg">是否确认删除?</string>
<string name="clear_font">默认字体</string> <string name="clear_font">默认字体</string>
@ -629,4 +631,7 @@
<string name="other_aloud_setting">其它朗读设置</string> <string name="other_aloud_setting">其它朗读设置</string>
<string name="system_typeface">系统内置字体样式</string> <string name="system_typeface">系统内置字体样式</string>
<string name="sure_delete_book_file">是否删除源文件</string> <string name="sure_delete_book_file">是否删除源文件</string>
<string name="default1">预设一</string>
<string name="default2">预设二</string>
<string name="default3">预设三</string>
</resources> </resources>

@ -37,6 +37,11 @@
android:title="@string/threads_num_title" android:title="@string/threads_num_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference
android:key="webPort"
android:title="@string/web_port_title"
app:iconSpaceReserved="false" />
<Preference <Preference
android:key="downloadPath" android:key="downloadPath"
android:title="@string/download_path" android:title="@string/download_path"

Loading…
Cancel
Save