Merge branch 'master' of https://github.com/gedoor/legado into 跟随主干

pull/172/head
wqfantexi 5 years ago
commit c18cebda9a
  1. 5
      app/build.gradle
  2. 45
      app/src/main/AndroidManifest.xml
  3. 129
      app/src/main/assets/help.md
  4. 43
      app/src/main/assets/updateLog.md
  5. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  6. 5
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  7. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  8. 14
      app/src/main/java/io/legado/app/data/entities/RssStar.kt
  9. 10
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  10. 46
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  11. 14
      app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt
  12. 2
      app/src/main/java/io/legado/app/help/permission/Request.kt
  13. 99
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  14. 1
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  15. 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  16. 3
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  17. 56
      app/src/main/java/io/legado/app/model/Debug.kt
  18. 12
      app/src/main/java/io/legado/app/model/Rss.kt
  19. 93
      app/src/main/java/io/legado/app/model/SearchBookModel.kt
  20. 47
      app/src/main/java/io/legado/app/model/WebBook.kt
  21. 8
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  22. 32
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  23. 4
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  24. 6
      app/src/main/java/io/legado/app/service/DownloadService.kt
  25. 21
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  26. 80
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  27. 5
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  28. 2
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  29. 6
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  30. 16
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt
  31. 7
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  32. 21
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  33. 7
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt
  34. 8
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  35. 2
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  36. 29
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  37. 32
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  38. 29
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  39. 26
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  40. 58
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  41. 100
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  42. 24
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  43. 4
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  44. 2
      app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt
  45. 6
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  46. 13
      app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt
  47. 7
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  48. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  49. 100
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  50. 7
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  51. 22
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  52. 12
      app/src/main/java/io/legado/app/ui/filechooser/FilePicker.kt
  53. 10
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  54. 8
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  55. 36
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfAdapter.kt
  56. 58
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  57. 6
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  58. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt
  59. 21
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  60. 12
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  61. 8
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  62. 5
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt
  63. 8
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt
  64. 14
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  65. 74
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  66. 17
      app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt
  67. 6
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  68. 7
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  69. 5
      app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt
  70. 110
      app/src/main/java/io/legado/app/ui/widget/SearchView.kt
  71. 185
      app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt
  72. 6
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  73. 41
      app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt
  74. 9
      app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt
  75. 21
      app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt
  76. 6
      app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt
  77. 7
      app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt
  78. 85
      app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt
  79. 29
      app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt
  80. 22
      app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt
  81. 2
      app/src/main/java/io/legado/app/utils/DocumentUtils.kt
  82. 66
      app/src/main/java/io/legado/app/utils/JsoupExtensions.kt
  83. 8
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  84. 24
      app/src/main/res/drawable-v24/ic_launcher_2.xml
  85. 56
      app/src/main/res/drawable-v24/ic_launcher_2b.xml
  86. 7
      app/src/main/res/drawable/bg_find_book_group.xml
  87. 10
      app/src/main/res/drawable/bg_prefs_color.xml
  88. 11
      app/src/main/res/drawable/bg_searchview.xml
  89. 9
      app/src/main/res/drawable/ic_arrow_down.xml
  90. 9
      app/src/main/res/drawable/ic_arrow_right.xml
  91. 5
      app/src/main/res/drawable/ic_bottom_books.xml
  92. 12
      app/src/main/res/drawable/ic_bottom_books_e.xml
  93. 12
      app/src/main/res/drawable/ic_bottom_books_s.xml
  94. 5
      app/src/main/res/drawable/ic_bottom_explore.xml
  95. 12
      app/src/main/res/drawable/ic_bottom_explore_e.xml
  96. 9
      app/src/main/res/drawable/ic_bottom_explore_s.xml
  97. 5
      app/src/main/res/drawable/ic_bottom_person.xml
  98. 12
      app/src/main/res/drawable/ic_bottom_person_e.xml
  99. 12
      app/src/main/res/drawable/ic_bottom_person_s.xml
  100. 5
      app/src/main/res/drawable/ic_bottom_rss_feed.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -136,13 +136,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.4'
def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
//paging
implementation 'androidx.paging:paging-runtime:2.1.1'
implementation 'androidx.paging:paging-runtime:2.1.2'
//anko
def anko_version = '0.10.8'

@ -23,6 +23,7 @@
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口-->
@ -78,6 +79,48 @@
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标4-->
<activity
android:name=".ui.welcome.Launcher4"
android:icon="@mipmap/launcher4"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标5-->
<activity
android:name=".ui.welcome.Launcher5"
android:icon="@mipmap/launcher5"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标6-->
<activity
android:name=".ui.welcome.Launcher6"
android:icon="@mipmap/launcher6"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--主界面-->
<activity
android:name=".ui.main.MainActivity"
@ -91,7 +134,7 @@
<!--书籍详情页-->
<activity
android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTask" />
android:launchMode="singleTop" />
<!--书籍信息编辑-->
<activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"

@ -0,0 +1,129 @@
## 常见问题
1.为什么第一次安装好之后什么东西都没有?
* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从QQ群、公众号“开源阅读软件”、酷安评论里获取由书友制作分享的书源。
2.如何导入本地书源文件?
* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入);
* 打开阅读软件;
* 我的 - 点击“书源管理”;
* 点击右上角选择“本地导入”;
* 左下角选择书源文件所在的路径;
* 点击书源文件导入;
* 导入后返回书源管理界面;
* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/
3.如何新建大佬发的单独书源?
* 复制书源代码;
* 打开阅读软件;
* 我的 - 点击“书源管理”;
* 右上角选择“新建书源”;
* 进入新建书源后点击右上角“粘贴源”;
* 粘贴书源完成后点击上方保存;
* 本次新建单独书源操作完成。
* 注:如果书源有错误或者复制不全会显示格式错误,请重新复制。
4.为什么导入2.0书源后看不了书?
* 2.0部分书源并不适用3.0,建议导入后进行筛选。
5.阅读2.0数据如何导入阅读3.0?
* 先对阅读2.0的数据进行备份,然后进入阅读3.0,点击“我的”,选择“备份与恢复”,再点击“导入旧版本数据”。
6.如何给朋友分享我的书源?
* 打开阅读软件;
* 点击备份;
* 打开手机自带的文件管理;
* 手机自带内存根目录找到YueDu3.0文件夹;
* 找到myBookSource.json长按选择分享;
* 选择微信分享或者QQ分享;
* 选择你要分享的好友点击发送;
* 好友接收后在手机自带内存根目录找到myBookSource.json文件(QQ在tencent--QQfile_recv微信在Tencent--MicroMsg--Download);
* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹);
* 打开阅读软件点击恢复。
* 注:备份路径如已修改过请在修改后的路径下查找书源文件。
7.目前阅读支持哪些格式的本地书籍?
* 目前支持TXT、EPUB格式(只支持显示EPUB里的文本内容,还不支持显示图片)。
8.如何刷新书架?
* 在书架界面下拉即可刷新。
9.书架界面书籍右上角的红色或者灰色背景小数字代表什么?
* 红色代表书籍有更新,灰色代表无更新,数字代表未读章节。
10.如何查看书籍详情?
* 长按书籍。
11.如何对书架上的书进行删除、切换书架的操作?
* 书籍详情页操作即可。
12.如何禁止或允许某本书更新?
* 书籍详情页,点击右上角 - “允许更新”。
13.如何更换小说封面、名字、作者或简介?
* 书籍详情页,点击右上角修改按钮。
14.怎么使用自定义字体?
* 阅读界面 - 字体-点击右上角选择字体文件路径。
15.目前支持哪些格式的字体文件?
* 目前支持ttf、otf格式。
16.书籍经常“正在加载中”怎么办?
* 在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。
17.书籍内容只有标题,正文内容是路径怎么办?
* 通常是缓存路径引起的,更换缓存路径即可。
18.效验书源显示失效就说明书源不能用了吗?
* 效验书源只是测试书源,可以做个参考,失效了不代表书源不能用了。
19.发现和正版书源能不能使用?
* 发现和正版书源只能用来找书,看排行榜,不能用来看书,如需看书请切换书源。
20.替换净化是什么?
* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。
21.如何自己填写净化替换规则?
* 第一行:替换规则名称 - 根据自己需求对替换净化规则进行命名;
* 第二行:分组 - 净化规则的分组组别;
* 第三行:替换规则 - 填写需要被替换的内容;
* 第四行:替换为 - 填写想替换成的内容(如不填则默认表示删除第二行里填写的内容);
* 第五行:替换范围,选填书名或者源名 - 填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。
* 注:如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第二行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行百度学习)。
22.如何听书?
* 可以使用手机自带的朗读引擎,也可使用第三方如谷歌、小米等朗读引擎。
* 【具体操作:安装-系统设置-其他高级设置-辅助功能-TTS输出-选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。】
23.如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页、自定义翻页案件?
* 阅读界面,设置(可上划,下面还有其他设置)。
24.搜索的时候感觉手机卡顿,如何解决?
* 我的 - 其他设置 - “更新和搜索线程数”调低。
25.更新前有什么注意事项?
* 要做好备份。
26.看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办?
* 一般是书源问题,切换书源即可。
27.为什么书源这么多,发现里却只有一点点?
* 书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。
28.云备份在哪?
* 我的 - 备份与恢复 - WebDav设置。
29.如何操作进行云备份?
* 侧栏设置,WebDav设置;
* 正确填写WebDAV 服务器地址、WebDAV 账号、WebDAV 密码;(要获得这三项的信息,需要注册一个坚果云账号,如果直接在手机上注册,坚果云会让你下载app,过程比较麻烦,为了一步到位,最好是在电脑上打开这个注册链接:https://www.jianguoyun.com/d/signup;注册后,进入坚果云;点击右上角账户名处选择 “账户信息”,然后选择“安全选项”;在“安全选项” 中找到“第三方应用管理”,并选择“添加应用”,输入名称如“阅读”后,会生成密码,选择完成;其中https://dav.jianguoyun.com/dav/就是填入“WebDAV 服务器地址”的内容,“使用情况”后面的邮箱地址就是你的“WebDAV 账号”,点击显示密码后得到的密码就是你的“WebDAV 密码”。)
* 无需操作,APP默认每天自动云备份一次。
30.关于云备份的相关说明
* 在正确设置好云备份的情况下,APP默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。
31.本地备份和云备份都能备份哪些东西?
* 书架、看书进度、搜索记录、书源、替换、APP设置等都会备份,基本涵盖所有内容。
32.出现某些未知bug怎么办?
* 清除软件数据试试看,不行再进行反馈。

@ -1,12 +1,51 @@
## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
**2020/03/21**
* 详情页点击书名搜索
**2020/03/20**
* 自动备份文件和手动备份文件分开
* 修复一些rss收藏取消不了的bug
* 修复rss请求头无效bug
**2020/03/19**
* 美化界面我的 by yangyxd
* 优化搜索
**2020/03/18**
* 尝试修复搜索时崩溃
* 解决看过书籍的移到顶部需要向上滚动才能看到的bug
* 只有再书源被删除找不到书源时才会自动换源
* 美化界面 by yangyxd
* 订阅后台播放
**2020/03/16**
* 修复滚动模式切换章节位置不归0的bug
* 修复文字选择更多菜单在部分手机上报错的bug
* 修复文字选择菜单问题
**2020/03/15**
* 加载正文无书源时自动换源
**2020/03/14**
* 修改导航栏图标
**2020/03/13**
* 更改书架控件,ViewPager2替换回2.0使用的ViewPager,解决下拉不流畅问题
* 修复点击作者搜索后,打开的详情页还是原来的书籍的bug
* 修改朗读菜单
* 优化rss朗读
**2020/03/12**
* 导入本地添加需要权限模式
**2020/03/11**
* 修复调节上边距时下边距一起动的bug
* 适配沚水的web阅读 by 六月
* 分组管理页面调整 by yangyxd
* 导入本地添加需要权限模式
**2020/03/10**
* 优化文字选择菜单弹出位置

@ -11,7 +11,6 @@ object EventBus {
const val TIME_CHANGED = "timeChanged"
const val UP_CONFIG = "upConfig"
const val OPEN_CHAPTER = "openChapter"
const val REPLACE = "replace"
const val AUDIO_SUB_TITLE = "audioSubTitle"
const val AUDIO_STATE = "audioState"
const val AUDIO_PROGRESS = "audioProgress"

@ -50,9 +50,12 @@ interface BookSourceDao {
@get:Query("select * from book_sources order by customOrder asc")
val all: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 order by customOrder asc")
@get:Query("select * from book_sources where enabled = 1 order by customOrder")
val allEnabled: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
val allTextEnabled: List<BookSource>
@Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource?

@ -31,7 +31,7 @@ data class Book(
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType
var type: Int = 0, // 0:text 1:audio
var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间

@ -16,4 +16,16 @@ data class RssStar(
var description: String? = null,
var content: String? = null,
var image: String? = null
)
) {
fun toRssArticle(): RssArticle {
return RssArticle(
origin = origin,
title = title,
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image
)
}
}

@ -5,10 +5,7 @@ import android.content.pm.PackageManager
import android.os.Build
import io.legado.app.App
import io.legado.app.R
import io.legado.app.ui.welcome.Launcher1
import io.legado.app.ui.welcome.Launcher2
import io.legado.app.ui.welcome.Launcher3
import io.legado.app.ui.welcome.WelcomeActivity
import io.legado.app.ui.welcome.*
import org.jetbrains.anko.toast
/**
@ -20,7 +17,10 @@ object LauncherIconHelp {
private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name)
ComponentName(App.INSTANCE, Launcher3::class.java.name),
ComponentName(App.INSTANCE, Launcher4::class.java.name),
ComponentName(App.INSTANCE, Launcher5::class.java.name),
ComponentName(App.INSTANCE, Launcher6::class.java.name)
)
fun changeIcon(icon: String?) {

@ -6,7 +6,7 @@ import kotlin.coroutines.CoroutineContext
class Coroutine<T>(
scope: CoroutineScope,
val scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
) {
@ -28,9 +28,10 @@ class Coroutine<T>(
private val job: Job
private var start: VoidCallback? = null
private var success: Callback<T?>? = null
private var success: Callback<T>? = null
private var error: Callback<Throwable>? = null
private var finally: VoidCallback? = null
private var cancel: VoidCallback? = null
private var timeMillis: Long? = null
private var errorReturn: Result<T>? = null
@ -45,7 +46,7 @@ class Coroutine<T>(
get() = job.isCompleted
init {
this.job = executeInternal(scope, context, block)
this.job = executeInternal(context, block)
}
fun timeout(timeMillis: () -> Long): Coroutine<T> {
@ -78,7 +79,7 @@ class Coroutine<T>(
fun onSuccess(
context: CoroutineContext? = null,
block: suspend CoroutineScope.(T?) -> Unit
block: suspend CoroutineScope.(T) -> Unit
): Coroutine<T> {
this.success = Callback(context, block)
return this@Coroutine
@ -100,9 +101,28 @@ class Coroutine<T>(
return this@Coroutine
}
fun onCancel(
context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit
): Coroutine<T> {
this.cancel = VoidCallback(context, block)
return this@Coroutine
}
//取消当前任务
fun cancel(cause: CancellationException? = null) {
job.cancel(cause)
cancel?.let {
MainScope().launch {
if (null == it.context) {
it.block.invoke(scope)
} else {
withContext(scope.coroutineContext.plus(it.context)) {
it.block.invoke(this)
}
}
}
}
}
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {
@ -110,7 +130,6 @@ class Coroutine<T>(
}
private fun executeInternal(
scope: CoroutineScope,
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): Job {
@ -118,21 +137,27 @@ class Coroutine<T>(
try {
start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, context, timeMillis ?: 0L, block)
success?.let { dispatchCallback(this, value, it) }
if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
val consume: Boolean = errorReturn?.value?.let { value ->
success?.let { dispatchCallback(this, value, it) }
if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
true
} ?: false
if (!consume) {
if (!consume && isActive) {
error?.let { dispatchCallback(this, e, it) }
}
} finally {
finally?.let { dispatchVoidCallback(this, it) }
if (isActive) {
finally?.let { dispatchVoidCallback(this, it) }
}
}
}
}
@ -152,6 +177,7 @@ class Coroutine<T>(
value: R,
callback: Callback<R>
) {
if (!scope.isActive) return
if (null == callback.context) {
callback.block.invoke(scope, value)
} else {
@ -166,7 +192,7 @@ class Coroutine<T>(
context: CoroutineContext,
timeMillis: Long,
noinline block: suspend CoroutineScope.() -> T
): T? {
): T {
return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) {
block()

@ -1,8 +1,11 @@
package io.legado.app.help.permission
import android.os.Build
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.jetbrains.anko.startActivity
import java.util.ArrayList
class PermissionsCompat private constructor() {
@ -12,6 +15,17 @@ class PermissionsCompat private constructor() {
RequestManager.pushRequest(request)
}
companion object {
// 检查权限, 如果已经拥有返回 true
fun check(activity: AppCompatActivity, vararg permissions: String): Boolean {
var request: Request = Request(activity)
var pers = ArrayList<String>()
pers.addAll(listOf(*permissions))
var data = request.getDeniedPermissions(pers.toTypedArray())
return data == null;
}
}
class Builder {
private val request: Request

@ -102,7 +102,7 @@ internal class Request : OnRequestPermissionsResultCallback {
deniedCallback = null
}
private fun getDeniedPermissions(permissions: Array<String>?): Array<String>? {
fun getDeniedPermissions(permissions: Array<String>?): Array<String>? {
if (permissions != null) {
val deniedPermissionList = ArrayList<String>()
for (permission in permissions) {

@ -21,14 +21,6 @@ object Backup {
FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath
}
val legadoPath by lazy {
FileUtils.getSdCardPath() + File.separator + "YueDu3.0"
}
val exportPath by lazy {
legadoPath + File.separator + "Export"
}
val backupFileNames by lazy {
arrayOf(
"bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
@ -42,47 +34,46 @@ object Backup {
return
}
Coroutine.async {
val backupPath = context.getPrefString(PreferKey.backupPath)
if (backupPath.isNullOrEmpty()) {
backup(context)
} else {
backup(context, backupPath)
context.getPrefString(PreferKey.backupPath)?.let {
backup(context, it, true)
}
}
}
suspend fun backup(context: Context, path: String = legadoPath) {
suspend fun backup(context: Context, path: String, isAuto: Boolean = false) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map {
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
synchronized(this@Backup) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map {
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
}
}
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path), isAuto)
} else {
copyBackup(File(path), isAuto)
}
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path))
} else {
copyBackup(File(path))
}
}
}
@ -95,27 +86,39 @@ object Backup {
}
@Throws(java.lang.Exception::class)
private fun copyBackup(context: Context, uri: Uri) {
private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
if (isAuto) {
treeDoc.findFile("auto")?.findFile(fileName)?.delete()
DocumentUtils.createFileIfNotExist(
treeDoc,
fileName,
subDirs = *arrayOf("auto")
)?.writeBytes(context, file.readBytes())
} else {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
}
}
}
}
}
@Throws(java.lang.Exception::class)
private fun copyBackup(rootFile: File) {
private fun copyBackup(rootFile: File, isAuto: Boolean) {
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(
FileUtils.createFileIfNotExist(rootFile, fileName),
true
if (isAuto) {
FileUtils.createFileIfNotExist(rootFile, fileName, "auto")
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
)
}
}

@ -40,7 +40,6 @@ object OldBook {
book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0

@ -128,7 +128,7 @@ object Restore {
bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
}
ChapterProvider.upStyle()
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
withContext(Main) {
if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {

@ -199,13 +199,12 @@ object ATH {
fun applyBottomNavigationColor(bottomBar: BottomNavigationView?) {
bottomBar?.apply {
setBackgroundColor(ThemeStore.backgroundColor(context))
setBackgroundColor(ThemeStore.bottomBackground(context))
val colorStateList = Selector.colorBuild()
.setDefaultColor(context.getCompatColor(R.color.btn_bg_press_tp))
.setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create()
itemIconTintList = colorStateList
itemTextColor = colorStateList
setBackgroundColor(ThemeStore.bottomBackground(context))
}
}

@ -56,9 +56,7 @@ object Debug {
log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null)
.onSuccess {
if (it == null) {
log(debugSource, "︽解析失败", state = -1)
} else if(it.articles.isEmpty()) {
if (it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空")
log(debugSource, "︽解析完成", state = 1000)
} else {
@ -69,7 +67,7 @@ object Debug {
if (ruleContent.isNullOrEmpty()) {
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
} else {
rssContentDebug(it.articles[0], ruleContent)
rssContentDebug(it.articles[0], ruleContent, rssSource)
}
} else {
log(debugSource, "⇒存在描述规则,不解析内容页")
@ -82,9 +80,9 @@ object Debug {
}
}
private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String) {
private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String, rssSource: RssSource) {
log(debugSource, "︾开始解析内容页")
Rss.getContent(rssArticle, ruleContent)
Rss.getContent(rssArticle, ruleContent, rssSource)
.onSuccess {
log(debugSource, it)
log(debugSource, "︽内容页解析完成", state = 1000)
@ -140,14 +138,12 @@ object Debug {
log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1)
.onSuccess { exploreBooks ->
exploreBooks?.let {
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
.onError {
@ -164,14 +160,12 @@ object Debug {
log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1)
.onSuccess { searchBooks ->
searchBooks?.let {
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
.onError {
@ -197,16 +191,14 @@ object Debug {
private fun tocDebug(webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book)
.onSuccess { chapterList ->
chapterList?.let {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(webBook, book, it[0], nextChapterUrl)
} else {
log(debugSource, "︽目录列表为空", state = -1)
}
.onSuccess {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(webBook, book, it[0], nextChapterUrl)
} else {
log(debugSource, "︽目录列表为空", state = -1)
}
}
.onError {

@ -21,7 +21,10 @@ object Rss {
context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(pageUrl ?: rssSource.sourceUrl)
val analyzeUrl = AnalyzeUrl(
pageUrl ?: rssSource.sourceUrl,
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource)
}
@ -30,12 +33,15 @@ object Rss {
fun getContent(
rssArticle: RssArticle,
ruleContent: String,
rssSource: RssSource?,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
val body = AnalyzeUrl(rssArticle.link, baseUrl = rssArticle.origin)
.getResponseAwait(rssArticle.origin)
val body = AnalyzeUrl(
rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap()
).getResponseAwait(rssArticle.origin)
.body
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(

@ -0,0 +1,93 @@
package io.legado.app.model
import io.legado.app.App
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var mSearchId = System.currentTimeMillis()
private var searchPage = 1
private var searchKey: String = ""
private var task: Coroutine<*>? = null
private fun initSearchPool() {
searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
}
fun search(searchId: Long, key: String) {
if (searchId != mSearchId) {
task?.cancel()
searchPool.close()
initSearchPool()
mSearchId = searchId
searchPage = 1
if (key.isEmpty()) {
return
} else {
this.searchKey = key
}
} else {
searchPage++
}
task = Coroutine.async(scope, searchPool) {
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
if (searchId == mSearchId) {
callBack.onSearchSuccess(it)
}
}
}
}.onStart {
callBack.onSearchStart()
}
task?.invokeOnCompletion {
if (searchId == mSearchId) {
callBack.onSearchFinish()
}
}
}
fun cancelSearch() {
task?.cancel()
mSearchId = 0
callBack.onSearchCancel()
}
fun close() {
task?.cancel()
mSearchId = 0
searchPool.close()
}
interface CallBack {
fun onSearchStart()
fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
fun onSearchFinish()
fun onSearchCancel()
}
}

@ -27,26 +27,36 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) {
bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl,
key = key,
page = page,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
} ?: arrayListOf()
searchBookSuspend(scope, key, page)
}
}
suspend fun searchBookSuspend(
scope: CoroutineScope,
key: String,
page: Int? = 1
): ArrayList<SearchBook> {
bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl,
key = key,
page = page,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
}
return arrayListOf()
}
/**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,

@ -2,6 +2,7 @@ package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
@ -39,7 +40,12 @@ object BookContent {
var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource
)
content.append(contentData.content.replace(bookChapter.title, "")).append("\n")
if (bookSource.bookSourceType == BookType.default) {
content.append(contentData.content.replace(bookChapter.title, "")).append("\n")
} else {
content.append(contentData.content).append("\n")
}
if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())

@ -9,11 +9,15 @@ import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
object BookList {
@Throws(Exception::class)
fun analyzeBookList(
scope: CoroutineScope,
body: String?,
bookSource: BookSource,
analyzeUrl: AnalyzeUrl,
@ -28,12 +32,13 @@ object BookList {
)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(null)
analyzeRule.setContent(body, baseUrl)
bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook ->
getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body
bookList.add(searchBook)
}
@ -59,7 +64,7 @@ object BookList {
collections = analyzeRule.getElements(ruleList)
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook ->
getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body
bookList.add(searchBook)
}
@ -74,8 +79,9 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem(
item, analyzeRule, bookSource, baseUrl, index == 0,
scope, item, analyzeRule, bookSource, baseUrl, index == 0,
ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,
ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,
ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount
@ -93,7 +99,9 @@ object BookList {
return bookList
}
@Throws(Exception::class)
private fun getInfoItem(
scope: CoroutineScope,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
baseUrl: String
@ -108,29 +116,37 @@ object BookList {
with(bookSource.getBookInfoRule()) {
init?.let {
if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it))
}
}
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = analyzeRule.getString(name)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -140,7 +156,9 @@ object BookList {
return null
}
@Throws(Exception::class)
private fun getSearchItem(
scope: CoroutineScope,
item: Any,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
@ -162,30 +180,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook
analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = analyzeRule.getString(ruleName)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
}
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) {

@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
@ -275,7 +275,7 @@ class AudioPlayService : BaseService(),
AudioPlay.book?.let { book ->
AudioPlay.webBook?.getContent(book, chapter, scope = this)
?.onSuccess(IO) { content ->
if (content.isNullOrEmpty()) {
if (content.isEmpty()) {
withContext(Main) {
toast("未获取到资源链接")
}

@ -130,10 +130,8 @@ class DownloadService : BaseService() {
// notificationContent = "启动:" + chapter.title
//}
.onSuccess(IO) { content ->
content?.let {
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
.onFinally(IO) {
synchronized(this@DownloadService) {

@ -29,7 +29,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
}
}
private var ttsIsSuccess: Boolean = false
private var ttsInitFinish = false
override fun onCreate() {
super.onCreate()
@ -42,22 +42,23 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
clearTTS()
}
@Synchronized
override fun onInit(status: Int) {
launch {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsIsSuccess = true
play()
} else {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsInitFinish = true
play()
} else {
launch {
toast(R.string.tts_init_failed)
}
}
}
@Suppress("DEPRECATION")
@Synchronized
override fun play() {
if (contentList.isEmpty() || !ttsIsSuccess) {
if (contentList.isEmpty() || !ttsInitFinish) {
return
}
if (requestFocus()) {

@ -21,7 +21,6 @@ import org.jetbrains.anko.toast
object ReadBook {
var titleDate = MutableLiveData<String>()
var book: Book? = null
var inBookshelf = false
@ -34,9 +33,10 @@ object ReadBook {
var curTextChapter: TextChapter? = null
var nextTextChapter: TextChapter? = null
var webBook: WebBook? = null
var msg: String? = null
private val loadingChapters = arrayListOf<Int>()
fun resetData(book: Book) {
fun resetData(book: Book, noSource: (name: String, author: String) -> Unit) {
this.book = book
titleDate.postValue(book.name)
durChapterIndex = book.durChapterIndex
@ -46,12 +46,27 @@ object ReadBook {
prevTextChapter = null
curTextChapter = null
nextTextChapter = null
upWebBook(book.origin)
upWebBook(book, noSource)
}
fun upWebBook(origin: String) {
val bookSource = App.db.bookSourceDao().getBookSource(origin)
webBook = if (bookSource != null) WebBook(bookSource) else null
fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) {
book ?: return
if (book.origin == BookType.local) {
webBook = null
} else {
val bookSource = App.db.bookSourceDao().getBookSource(book.origin)
if (bookSource != null) {
webBook = WebBook(bookSource)
} else {
webBook = null
noSource.invoke(book.name, book.author)
}
}
}
fun upMsg(msg: String?) {
this.msg = msg
callBack?.upContent()
}
fun moveToNextPage() {
@ -69,11 +84,11 @@ object ReadBook {
nextTextChapter = null
book?.let {
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent)
loadContent(durChapterIndex, upContent, false)
} else if (upContent) {
callBack?.upContent()
}
loadContent(durChapterIndex.plus(1), upContent)
loadContent(durChapterIndex.plus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) {
for (i in 2..10) {
delay(100)
@ -99,11 +114,11 @@ object ReadBook {
prevTextChapter = null
book?.let {
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent)
loadContent(durChapterIndex, upContent, false)
} else if (upContent) {
callBack?.upContent()
}
loadContent(durChapterIndex.minus(1), upContent)
loadContent(durChapterIndex.minus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) {
for (i in -5..-2) {
delay(100)
@ -184,21 +199,21 @@ object ReadBook {
/**
* 加载章节内容
*/
fun loadContent() {
loadContent(durChapterIndex)
loadContent(durChapterIndex + 1)
loadContent(durChapterIndex - 1)
fun loadContent(resetPageOffset: Boolean) {
loadContent(durChapterIndex, resetPageOffset = resetPageOffset)
loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
}
fun loadContent(index: Int, upContent: Boolean = true) {
fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) {
book?.let { book ->
if (addLoading(index)) {
Coroutine.async {
App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter ->
BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(chapter, it, upContent)
contentLoadFinish(chapter, it, upContent, resetPageOffset)
removeLoading(chapter.index)
} ?: download(chapter)
} ?: download(chapter, resetPageOffset = resetPageOffset)
} ?: removeLoading(index)
}.onError {
removeLoading(index)
@ -216,7 +231,7 @@ object ReadBook {
if (BookHelp.hasContent(book, chapter)) {
removeLoading(chapter.index)
} else {
download(chapter)
download(chapter, false)
}
} ?: removeLoading(index)
}.onError {
@ -226,20 +241,28 @@ object ReadBook {
}
}
private fun download(chapter: BookChapter) {
private fun download(chapter: BookChapter, resetPageOffset: Boolean) {
book?.let { book ->
webBook?.getContent(book, chapter)
?.onSuccess(Dispatchers.IO) { content ->
if (content.isNullOrEmpty()) {
contentLoadFinish(chapter, App.INSTANCE.getString(R.string.content_empty))
if (content.isEmpty()) {
contentLoadFinish(
chapter,
App.INSTANCE.getString(R.string.content_empty),
resetPageOffset = resetPageOffset
)
removeLoading(chapter.index)
} else {
BookHelp.saveContent(book, chapter, content)
contentLoadFinish(chapter, content)
contentLoadFinish(chapter, content, resetPageOffset = resetPageOffset)
removeLoading(chapter.index)
}
}?.onError {
contentLoadFinish(chapter, it.localizedMessage ?: "未知错误")
contentLoadFinish(
chapter,
it.localizedMessage ?: "未知错误",
resetPageOffset = resetPageOffset
)
removeLoading(chapter.index)
}
}
@ -265,7 +288,8 @@ object ReadBook {
private fun contentLoadFinish(
chapter: BookChapter,
content: String,
upContent: Boolean = true
upContent: Boolean = true,
resetPageOffset: Boolean
) {
Coroutine.async {
if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) {
@ -279,18 +303,18 @@ object ReadBook {
when (chapter.index) {
durChapterIndex -> {
curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent()
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView()
curPageChanged()
callBack?.contentLoadFinish()
}
durChapterIndex - 1 -> {
prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent(-1)
if (upContent) callBack?.upContent(-1, resetPageOffset)
}
durChapterIndex + 1 -> {
nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent(1)
if (upContent) callBack?.upContent(1, resetPageOffset)
}
}
}
@ -316,7 +340,7 @@ object ReadBook {
}
interface CallBack {
fun upContent(relativePosition: Int = 0)
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upView()
fun upPageProgress()
fun contentLoadFinish()

@ -46,9 +46,8 @@ class AudioPlayActivity :
private var adjustProgress = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
AudioPlay.titleData.observe(this, Observer { toolbar.title = it })
title_bar.transparent()
AudioPlay.titleData.observe(this, Observer { title_bar.title = it })
AudioPlay.coverData.observe(this, Observer { upCover(it) })
viewModel.initData(intent)
initView()

@ -72,7 +72,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
execute {
AudioPlay.webBook?.getChapterList(book, this)
?.onSuccess(Dispatchers.IO) { cList ->
if (!cList.isNullOrEmpty()) {
if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray())
AudioPlay.chapterSize = cList.size

@ -6,12 +6,14 @@ import android.view.MenuItem
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
@ -64,6 +66,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
select_action_bar.setMainActionText(R.string.move_to_group)
select_action_bar.inflateMenu(R.menu.arrange_book_sel)
select_action_bar.setOnMenuItemClickListener(this)

@ -7,14 +7,13 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import io.legado.app.help.ItemTouchCallback
import kotlinx.android.synthetic.main.item_arrange_book.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book) {
SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book),
ItemTouchCallback.OnItemTouchCallbackListener {
val groupRequestCode = 12
private val selectedBooks: HashSet<Book> = hashSetOf()
var actionItem: Book? = null
@ -56,12 +55,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {
tv_name.text = item.name
// tv_name.text = if (item.author.isEmpty()) {
// item.name
// } else {
// "${item.name}(${item.author})"
// }
tv_author.text = item.author // resources.getString(R.string.author_show, item.author)
tv_author.text = item.author
tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE
tv_group_s.text = getGroupName(item.group)
checkbox.isChecked = selectedBooks.contains(item)
@ -115,7 +109,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
groupNames.add(it.groupName)
}
}
return groupNames;
return groupNames
}
private fun getGroupName(groupId: Int): String {

@ -51,14 +51,13 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
fun search() {
task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
.onSuccess(Dispatchers.IO) {
if (it != null && it.isNotEmpty()) {
if (it.isNotEmpty()) {
val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty()
@ -72,6 +71,10 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
}
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
}
task?.invokeOnCompletion {

@ -72,14 +72,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
fun search() {
task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
.onSuccess(IO) {
it?.forEach { searchBook ->
it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) {
@ -95,6 +94,10 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
}
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
}
task?.invokeOnCompletion {
@ -107,7 +110,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess {
it?.let { loadChapter(it) }
loadChapter(it)
}.onError {
debug { context.getString(R.string.error_get_book_info) }
}
@ -119,13 +122,11 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) {
it?.let { chapters ->
if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook()
searchFinish(searchBook)
}
.onSuccess(IO) { chapters ->
if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook()
searchFinish(searchBook)
}
}.onError {
debug { context.getString(R.string.error_get_chapter_list) }

@ -36,6 +36,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
private var durChapterIndex = 0
private lateinit var mLayoutManager: UpLinearLayoutManager
private var tocLiveData: LiveData<List<BookChapter>>? = null
private var scrollToDurChapter = false
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.chapterCallBack = this
@ -74,7 +75,6 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.let {
durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
initCacheFileNames(it)
}
}
@ -85,7 +85,10 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl)
tocLiveData?.observe(viewLifecycleOwner, Observer {
adapter.setItems(it)
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
if (!scrollToDurChapter) {
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
scrollToDurChapter = true
}
})
}

@ -35,11 +35,9 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
WebBook(source).exploreBook(url, page, this)
.timeout(30000L)
.onSuccess(IO) { searchBooks ->
searchBooks?.let {
booksData.postValue(searchBooks)
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
page++
}
booksData.postValue(searchBooks)
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
page++
}
}
}

@ -33,6 +33,7 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -74,6 +75,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() }
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult =

@ -1,11 +1,14 @@
package io.legado.app.ui.book.info
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ -29,6 +32,7 @@ import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.utils.dp
import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.visible
@ -53,8 +57,7 @@ class BookInfoActivity :
get() = getViewModel(BookInfoViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title_bar.transparent()
viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.chapterListData.observe(this, Observer { upLoading(false, it) })
viewModel.initData(intent)
@ -227,25 +230,33 @@ class BookInfoActivity :
tv_author.onClick {
startActivity<SearchActivity>(Pair("key", viewModel.bookData.value?.author))
}
tv_name.onClick {
startActivity<SearchActivity>(Pair("key", viewModel.bookData.value?.name))
}
}
@SuppressLint("InflateParams")
private fun deleteBook() {
viewModel.bookData.value?.let {
if (it.isLocalBook()) {
alert(
titleResource = R.string.sure,
messageResource = R.string.sure_delete_book_file
messageResource = R.string.sure_del
) {
positiveButton(R.string.yes) {
viewModel.delBook(true) {
finish()
}
val checkBox = CheckBox(this@BookInfoActivity).apply {
setText(R.string.delete_book_file)
}
val view = LinearLayout(this@BookInfoActivity).apply {
setPadding(16.dp, 0, 16.dp, 0)
addView(checkBox)
}
negativeButton(R.string.no) {
viewModel.delBook(false) {
customView = view
positiveButton(R.string.yes) {
viewModel.delBook(checkBox.isChecked) {
finish()
}
}
negativeButton(R.string.no)
}.show()
} else {
viewModel.delBook {

@ -59,13 +59,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) {
it?.let {
bookData.postValue(book)
if (inBookshelf) {
App.db.bookDao().update(book)
}
loadChapter(it, changeDruChapterIndex)
bookData.postValue(book)
if (inBookshelf) {
App.db.bookDao().update(book)
}
loadChapter(it, changeDruChapterIndex)
}.onError {
toast(R.string.error_get_book_info)
}
@ -92,20 +90,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) {
it?.let {
if (it.isNotEmpty()) {
if (inBookshelf) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
if (changeDruChapterIndex == null) {
chapterListData.postValue(it)
} else {
changeDruChapterIndex(it)
}
if (it.isNotEmpty()) {
if (inBookshelf) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
if (changeDruChapterIndex == null) {
chapterListData.postValue(it)
} else {
toast(R.string.chapter_list_empty)
changeDruChapterIndex(it)
}
} else {
toast(R.string.chapter_list_empty)
}
}.onError {
chapterListData.postValue(null)

@ -8,6 +8,7 @@ import android.os.Bundle
import android.provider.DocumentsContract
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
@ -17,6 +18,8 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.help.AppConfig
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.widget.SelectActionBar
@ -40,7 +43,8 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null
private var path = FileUtils.getSdCardPath()
private var sdPath = FileUtils.getSdCardPath()
private var path = sdPath
override val viewModel: ImportBookViewModel
get() = getViewModel(ImportBookViewModel::class.java)
@ -79,6 +83,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
}
}
})
}
private fun initEvent() {
@ -123,6 +128,24 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
subDocs.clear()
path = it
}
} ?: let {
// 没有权限就显示一个授权提示和按钮
if (PermissionsCompat.check(this, *Permissions.Group.STORAGE)) {
hint_per.visibility = View.GONE
} else {
hint_per.visibility = View.VISIBLE
tv_request_per.onClick {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
hint_per.visibility = View.GONE
initData()
upRootDoc()
}
.request()
}
}
}
upPath()
}
@ -159,7 +182,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
}
}
} ?: let {
tv_path.text = path
tv_path.text = path.replace(sdPath, "SD")
val docList = arrayListOf<DocItem>()
File(path).listFiles()?.forEach {
if (it.isDirectory) {
@ -228,7 +251,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
@Synchronized
private fun goBackDir(): Boolean {
if (rootDoc == null) {
if (path != FileUtils.getSdCardPath()) {
if (path != sdPath) {
File(path).parent?.let {
path = it
upPath()

@ -25,6 +25,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.SyncBookProgress
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
@ -91,6 +92,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onCreate(savedInstanceState: Bundle?) {
ReadBook.msg = null
Help.setOrientation(this)
super.onCreate(savedInstanceState)
}
@ -115,7 +117,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
override fun onResume() {
@ -402,11 +404,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/
override fun showTextActionMenu() {
textActionMenu ?: let {
textActionMenu = TextActionMenu(this, this).apply {
contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
}
textActionMenu = TextActionMenu(this, this)
}
textActionMenu?.let { popup ->
popup.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val popupHeight = popup.contentView.measuredHeight
val x = text_menu_position.x.toInt()
var y = text_menu_position.y.toInt() - popupHeight
@ -481,9 +482,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/**
* 更新内容
*/
override fun upContent(relativePosition: Int) {
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
launch {
page_view.upContent(relativePosition)
page_view.upContent(relativePosition, resetPageOffset)
}
}
@ -579,7 +580,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onReplaceRuleSave() {
Coroutine.async {
BookHelp.upReplaceRules()
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
}
@ -682,8 +683,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss()
page_view.onDestroy()
ReadBook.msg = null
if (!BuildConfig.DEBUG) {
SyncBookProgress.uploadBookProgress()
Backup.autoBack(this)
}
}
@ -707,9 +710,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upBg()
page_view.upStyle()
if (it) {
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
} else {
page_view.upContent()
page_view.upContent(resetPageOffset = false)
}
}
observeEvent<Int>(EventBus.ALOUD_STATE) {
@ -718,7 +721,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
val page = textChapter.page(ReadBook.durPageIndex)
if (page != null) {
page.removePageAloudSpan()
page_view.upContent()
page_view.upContent(resetPageOffset = false)
}
}
}
@ -735,9 +738,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
}
}
observeEvent<String>(EventBus.REPLACE) {
ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog")
}
observeEvent<Boolean>(PreferKey.keepLight) {
upScreenTimeOut()
}

@ -39,7 +39,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun initBook(book: Book) {
if (ReadBook.book?.bookUrl != book.bookUrl) {
ReadBook.resetData(book)
ReadBook.resetData(book) { name, author ->
autoChangeSource(name, author)
}
isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) {
@ -52,7 +54,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
}
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
@ -60,7 +62,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else {
isInitFinish = true
ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book.origin)
ReadBook.upWebBook(book) { name, author ->
autoChangeSource(name, author)
}
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) {
@ -70,7 +74,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
} else {
if (ReadBook.curTextChapter != null) {
ReadBook.callBack?.upContent()
ReadBook.callBack?.upContent(resetPageOffset = false)
}
}
}
@ -103,32 +107,34 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = it.size
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
} else {
ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) {
if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size
ReadBook.loadContent()
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
} else {
changeDruChapterIndex(cList)
}
} else {
toast(R.string.error_load_toc)
ReadBook.upMsg(context.getString(R.string.error_load_toc))
}
}?.onError {
toast(R.string.error_load_toc)
} ?: autoChangeSource()
ReadBook.upMsg(context.getString(R.string.error_load_toc))
}
}
}
}
fun changeTo(book1: Book) {
execute {
ReadBook.upMsg(null)
ReadBook.book?.let {
App.db.bookDao().delete(it)
}
@ -151,8 +157,26 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
}
private fun autoChangeSource() {
private fun autoChangeSource(name: String, author: String) {
execute {
App.db.bookSourceDao().allTextEnabled.forEach { source ->
try {
val searchBooks = WebBook(source).searchBookSuspend(this, name)
searchBooks.getOrNull(0)?.let {
if (it.name == name && (it.author == author || author == "")) {
changeTo(it.toBook())
return@forEach
}
}
} catch (e: Exception) {
//nothing
}
}
}.onStart {
ReadBook.upMsg("正在自动换源")
}.onFinally {
ReadBook.upMsg(null)
}
}
private fun upChangeDurChapterIndex(book: Book, chapters: List<BookChapter>) {
@ -167,7 +191,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray())
ReadBook.chapterSize = chapters.size
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
}
@ -181,7 +205,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durPageIndex = pageIndex
}
ReadBook.saveRead()
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
fun removeFromBookshelf(success: (() -> Unit)?) {
@ -208,9 +232,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
execute {
App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex)
?.let { chapter ->
BookHelp.delContent(book, chapter)
ReadBook.loadContent(ReadBook.durChapterIndex)
}
BookHelp.delContent(book, chapter)
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
}
}
}

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.speech.tts.TextToSpeech
import android.view.LayoutInflater
import android.view.Menu
import android.view.ViewGroup
@ -15,13 +16,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.core.view.isVisible
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.service.BaseReadAloudService
import io.legado.app.utils.gone
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.visible
@ -30,10 +30,16 @@ import kotlinx.android.synthetic.main.popup_action_menu.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.share
import org.jetbrains.anko.toast
import java.util.*
@SuppressLint("RestrictedApi")
class TextActionMenu(private val context: Context, private val callBack: CallBack) :
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT),
TextToSpeech.OnInitListener {
private val adapter = Adapter(context)
private val menu = MenuBuilder(context)
private val moreMenu = MenuBuilder(context)
init {
@SuppressLint("InflateParams")
@ -44,33 +50,38 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
isFocusable = false
initRecyclerView()
setOnDismissListener {
contentView.apply {
iv_menu_more.setImageResource(R.drawable.ic_more_vert)
recycler_view_more.gone()
adapter.setItems(menu.visibleItems)
recycler_view.visible()
}
}
}
private fun initRecyclerView() = with(contentView) {
val adapter = Adapter(context)
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
recycler_view.adapter = adapter
val menu = MenuBuilder(context)
recycler_view_more.adapter = adapter
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val popupMenu = PopupMenu(context, iv_menu_more)
onInitializeMenu(popupMenu.menu)
if (popupMenu.menu.size > 0) {
iv_menu_more.visible()
popupMenu.setOnMenuItemClickListener { item ->
item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
this@TextActionMenu.dismiss()
true
}
}
iv_menu_more.onClick {
popupMenu.show()
onInitializeMenu(moreMenu)
}
if (moreMenu.size() > 0) {
iv_menu_more.visible()
}
iv_menu_more.onClick {
if (recycler_view.isVisible) {
iv_menu_more.setImageResource(R.drawable.ic_arrow_back)
adapter.setItems(moreMenu.visibleItems)
recycler_view.gone()
recycler_view_more.visible()
} else {
iv_menu_more.setImageResource(R.drawable.ic_more_vert)
recycler_view_more.gone()
adapter.setItems(menu.visibleItems)
recycler_view.visible()
}
}
}
@ -104,6 +115,13 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(callBack.selectedText)
R.id.menu_aloud -> {
if (BaseReadAloudService.isRun) {
context.toast(R.string.alouding_disable)
return
}
readAloud(callBack.selectedText)
}
R.id.menu_browser -> {
try {
val intent = if (callBack.selectedText.isAbsUrl()) {
@ -121,9 +139,41 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
context.toast(e.localizedMessage ?: "ERROR")
}
}
else -> item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
}
}
private var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
private var lastText: String = ""
@SuppressLint("SetJavaScriptEnabled")
private fun readAloud(text: String) {
lastText = text
if (textToSpeech == null) {
textToSpeech = TextToSpeech(context, this)
return
}
if (!ttsInitFinish) return
if (text == "") return
if (textToSpeech?.isSpeaking == true)
textToSpeech?.stop()
textToSpeech?.speak(text, TextToSpeech.QUEUE_ADD, null, "select_text")
lastText = ""
}
@Synchronized
override fun onInit(status: Int) {
textToSpeech?.language = Locale.CHINA
ttsInitFinish = true
readAloud(lastText)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun createProcessTextIntent(): Intent {
return Intent()

@ -21,7 +21,6 @@ import io.legado.app.utils.observeEvent
import io.legado.app.utils.putPrefBoolean
import kotlinx.android.synthetic.main.dialog_read_aloud.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadAloudDialog : BaseDialogFragment() {
var callBack: CallBack? = null
@ -34,7 +33,7 @@ class ReadAloudDialog : BaseDialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes
attr.dimAmount = 0.0f
@ -54,10 +53,10 @@ class ReadAloudDialog : BaseDialogFragment() {
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
ll_bottom_bg.setBackgroundColor(requireContext().bottomBackground)
root_view.setBackgroundColor(requireContext().bottomBackground)
initOnChange()
initData()
initOnClick()
initEvent()
}
private fun initData() {
@ -101,22 +100,19 @@ class ReadAloudDialog : BaseDialogFragment() {
})
}
private fun initOnClick() {
iv_menu.onClick { callBack?.showMenuBar(); dismiss() }
iv_other_config.onClick {
private fun initEvent() {
ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() }
ll_setting.onClick {
ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog")
}
tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) }
tv_next.onClick { ReadBook.moveToNextChapter(true) }
iv_stop.onClick { ReadAloud.stop(requireContext()); dismiss() }
iv_play_pause.onClick { callBack?.onClickReadAloud() }
iv_play_prev.onClick { ReadAloud.prevParagraph(requireContext()) }
iv_play_prev.onLongClick {
ReadBook.moveToPrevChapter(upContent = true, toLast = false)
true
}
iv_play_next.onClick { ReadAloud.nextParagraph(requireContext()) }
iv_play_next.onLongClick { ReadBook.moveToNextChapter(true); true }
fabToc.onClick { callBack?.openChapterList() }
fabBack.onClick { callBack?.finish() }
ll_catalog.onClick { callBack?.openChapterList() }
ll_to_backstage.onClick { callBack?.finish() }
}
private fun upPlayState() {

@ -108,8 +108,10 @@ class ContentView(context: Context) : FrameLayout(context) {
}
}
fun setContent(textPage: TextPage) {
fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
setProgress(textPage)
if (resetPageOffset)
resetPageOffset()
content_text_view.setContent(textPage)
}

@ -17,5 +17,5 @@ interface DataSource {
fun hasPrevChapter(): Boolean
fun upContent(relativePosition: Int = 0)
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
}

@ -38,7 +38,7 @@ class PageView(context: Context, attrs: AttributeSet) :
prevPage.x = -w.toFloat()
pageDelegate?.setViewSize(w, h)
if (oldw != 0 && oldh != 0) {
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
}
@ -93,9 +93,9 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent()
}
override fun upContent(relativePosition: Int) {
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
if (ReadBookConfig.isScroll) {
curPage.setContent(pageFactory.currentPage)
curPage.setContent(pageFactory.currentPage, resetPageOffset)
} else {
when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage)

@ -38,7 +38,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else {
ReadBook.setPageIndex(pageIndex.plus(1))
}
if (upContent) upContent()
if (upContent) upContent(resetPageOffset = false)
true
} else
false
@ -51,7 +51,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else {
ReadBook.setPageIndex(pageIndex.minus(1))
}
if (upContent) upContent()
if (upContent) upContent(resetPageOffset = false)
true
} else
false
@ -59,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val currentPage: TextPage
get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let {
return@with it.page(pageIndex)
?: TextPage(title = it.title).format()
@ -68,6 +71,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val nextPage: TextPage
get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let {
if (pageIndex < it.pageSize() - 1) {
return@with it.page(pageIndex + 1)?.removePageAloudSpan()
@ -86,6 +92,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val prevPage: TextPage
get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
if (pageIndex > 0) {
currentChapter?.let {
return@with it.page(pageIndex - 1)?.removePageAloudSpan()

@ -49,9 +49,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle()
val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition]
if (oldItem.name != newItem.name) {
payload.putString("name", newItem.name)
}
@ -73,6 +73,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.intro != newItem.intro) {
payload.putString("intro", newItem.intro)
}
if (payload.isEmpty) {
return null
}
return payload
}
}

@ -112,6 +112,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
viewModel.saveSearchKey(query)
viewModel.search(it)
}
openOrCloseHistory(false)
return true
}
@ -128,6 +129,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
openOrCloseHistory(hasFocus)
}
}
openOrCloseHistory(true)
}
private fun initRecyclerView() {
@ -283,10 +285,12 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
*/
@Synchronized
private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) {
val searchItems = ArrayList(items)
if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
refreshTime = System.currentTimeMillis()
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items))
adapter.setItems(items, diffResult)
val diffResult =
DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), searchItems))
adapter.setItems(searchItems, diffResult)
}
}

@ -7,82 +7,62 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.model.SearchBookModel
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
class SearchViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
class SearchViewModel(application: Application) : BaseViewModel(application)
, SearchBookModel.CallBack {
private val searchBookModel = SearchBookModel(this, this)
var isSearchLiveData = MutableLiveData<Boolean>()
var searchBookLiveData = MutableLiveData<List<SearchBook>>()
var searchKey: String = ""
var searchPage = 1
var isLoading = false
var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L
/**
* 开始搜索
*/
fun search(key: String) {
task?.cancel()
if (key.isEmpty() && searchKey.isEmpty()) {
return
} else if (key.isEmpty()) {
isLoading = true
searchPage++
} else if (key.isNotEmpty()) {
isLoading = true
searchPage = 1
searchKey = key
if (searchKey != key && key.isNotEmpty()) {
searchBookModel.cancelSearch()
searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
}
searchBookModel.search(searchID, searchKey)
}
override fun onSearchStart() {
isSearchLiveData.postValue(true)
task = execute {
val searchGroup = context.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this@execute,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
it?.let { list ->
if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(list)
} else {
App.db.searchBookDao().insert(*list.toTypedArray())
mergeItems(list)
}
}
}
}
}
}
task?.invokeOnCompletion {
isSearchLiveData.postValue(false)
isLoading = false
override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(this, searchBooks)
} else {
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
mergeItems(this, searchBooks)
}
}
override fun onSearchFinish() {
isSearchLiveData.postValue(false)
isLoading = false
}
override fun onSearchCancel() {
isSearchLiveData.postValue(false)
isLoading = false
}
/**
* 精确搜索处理
*/
private fun precisionSearch(searchBooks: List<SearchBook>) {
private fun precisionSearch(scope: CoroutineScope, searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook ->
if (searchBook.name.contains(searchKey, true)
@ -90,14 +70,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
) books.add(searchBook)
}
App.db.searchBookDao().insert(*books.toTypedArray())
mergeItems(books)
if (scope.isActive) {
mergeItems(scope, books)
}
}
/**
* 合并搜索结果并排序
*/
@Synchronized
private fun mergeItems(newDataS: List<SearchBook>) {
private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>) {
if (newDataS.isNotEmpty()) {
val copyDataS = ArrayList(searchBooks)
val searchBooksAdd = ArrayList<SearchBook>()
@ -141,6 +123,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
}
}
}
if (!scope.isActive) return
searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) {
1
@ -166,6 +149,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
0
}
})
if (!scope.isActive) return
searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS)
}
@ -175,7 +159,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
* 停止搜索
*/
fun stop() {
task?.cancel()
searchBookModel.cancelSearch()
}
/**
@ -211,7 +195,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
override fun onCleared() {
super.onCleared()
searchPool.close()
searchBookModel.close()
}
}

@ -8,7 +8,6 @@ import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.OldRule
import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.*
@ -96,7 +95,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
FileUtils.createFileIfNotExist(file, "exportBookSource.json")
.writeText(json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${file.absolutePath}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}
@ -109,7 +108,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
doc.createFile("", "exportBookSource.json")
?.writeText(context, json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${doc.uri.path}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}
@ -211,7 +210,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}.onError {
finally(it.localizedMessage ?: "")
}.onSuccess {
finally(it ?: "导入完成")
finally(it)
}
}

@ -48,30 +48,24 @@ object BackupRestoreUi {
selectBackupFolder(fragment, backupSelectRequestCode)
}
} else {
backupUsePermission(fragment, requestCode = backupSelectRequestCode)
backupUsePermission(fragment, backupPath)
}
}
}
private fun backupUsePermission(
fragment: Fragment,
path: String = Backup.legadoPath,
requestCode: Int = selectFolderRequestCode
path: String
) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
when (requestCode) {
selectFolderRequestCode -> AppConfig.backupPath = Backup.legadoPath
else -> {
Coroutine.async {
AppConfig.backupPath = path
Backup.backup(fragment.requireContext(), path)
}.onSuccess {
fragment.toast(R.string.backup_success)
}
}
Coroutine.async {
AppConfig.backupPath = path
Backup.backup(fragment.requireContext(), path)
}.onSuccess {
fragment.toast(R.string.backup_success)
}
}
.request()
@ -107,7 +101,7 @@ object BackupRestoreUi {
}
}
private fun restoreUsePermission(fragment: Fragment, path: String = Backup.legadoPath) {
private fun restoreUsePermission(fragment: Fragment, path: String) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)

@ -31,7 +31,7 @@ object FilePicker {
0 -> default?.invoke()
1 -> {
try {
val intent = getSelectDirIntent()
val intent = createSelectDirIntent()
activity.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
@ -68,7 +68,7 @@ object FilePicker {
0 -> default?.invoke()
1 -> {
try {
val intent = getSelectDirIntent()
val intent = createSelectDirIntent()
fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
@ -106,7 +106,7 @@ object FilePicker {
0 -> default?.invoke()
1 -> {
try {
val intent = getSelectFileIntent()
val intent = createSelectFileIntent()
intent.type = type//设置类型
activity.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
@ -147,7 +147,7 @@ object FilePicker {
0 -> default?.invoke()
1 -> {
try {
val intent = getSelectFileIntent()
val intent = createSelectFileIntent()
intent.type = type//设置类型
fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
@ -168,14 +168,14 @@ object FilePicker {
}.show()
}
private fun getSelectFileIntent(): Intent {
private fun createSelectFileIntent(): Intent {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
return intent
}
private fun getSelectDirIntent(): Intent {
private fun createSelectDirIntent(): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
return intent

@ -15,7 +15,6 @@ import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
@ -78,7 +77,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
if (!BuildConfig.DEBUG) {
val log = String(assets.open("updateLog.md").readBytes())
TextDialog.show(supportFragmentManager, log, TextDialog.MD)
TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000)
}
}
}
@ -114,13 +113,6 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
return super.onKeyUp(keyCode, event)
}
override fun finish() {
if (!BuildConfig.DEBUG) {
Backup.autoBack(this)
}
super.finish()
}
override fun onDestroy() {
super.onDestroy()
ReadAloud.stop(this)

@ -33,11 +33,9 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
updateList.remove(book.bookUrl)
postEvent(EventBus.UP_BOOK, book.bookUrl)
}
it?.let {
App.db.bookDao().update(book)
App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
App.db.bookDao().update(book)
App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
.onError {
synchronized(this) {

@ -1,36 +0,0 @@
package io.legado.app.ui.main.bookshelf
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.main.bookshelf.books.BooksFragment
class BookshelfAdapter(fragment: Fragment, val callBack: CallBack) :
FragmentStateAdapter(fragment) {
private val ids = hashSetOf<Long>()
override fun getItemCount(): Int {
return callBack.groupSize
}
override fun getItemId(position: Int): Long {
return callBack.getGroup(position).groupId.toLong()
}
override fun containsItem(itemId: Long): Boolean {
return ids.contains(itemId)
}
override fun createFragment(position: Int): Fragment {
val groupId = callBack.getGroup(position).groupId
ids.add(groupId.toLong())
return BooksFragment.newInstance(position, groupId)
}
interface CallBack {
val groupSize: Int
fun getGroup(position: Int): BookGroup
}
}

@ -7,10 +7,12 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
@ -29,6 +31,7 @@ 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.search.SearchActivity
import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
@ -42,15 +45,14 @@ import org.jetbrains.anko.startActivity
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener,
SearchView.OnQueryTextListener,
GroupManageDialog.CallBack,
BookshelfAdapter.CallBack {
GroupManageDialog.CallBack {
override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java)
private lateinit var bookshelfAdapter: BookshelfAdapter
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Int, Fragment>()
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
setSupportToolbar(toolbar)
@ -79,26 +81,17 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
}
override val groupSize: Int
get() = bookGroups.size
override fun getGroup(position: Int): BookGroup {
return bookGroups[position]
}
private val selectedGroup: BookGroup
get() = bookGroups[view_pager_bookshelf.currentItem]
private fun initView() {
ATH.applyEdgeEffectColor(view_pager_bookshelf)
tab_layout.isTabIndicatorFullWidth = false
tab_layout.tabMode = TabLayout.MODE_SCROLLABLE
tab_layout.setSelectedTabIndicatorColor(requireContext().accentColor)
ATH.applyEdgeEffectColor(view_pager_bookshelf)
bookshelfAdapter = BookshelfAdapter(this, this)
view_pager_bookshelf.adapter = bookshelfAdapter
TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position ->
tab.text = bookGroups[position].groupName
}.attach()
tab_layout.setupWithViewPager(view_pager_bookshelf)
view_pager_bookshelf.offscreenPageLimit = 1
view_pager_bookshelf.adapter = TabFragmentPageAdapter(childFragmentManager)
}
private fun initBookGroupData() {
@ -119,7 +112,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
bookGroups.add(AppConst.bookGroupAudio)
}
bookGroups.addAll(it)
bookshelfAdapter.notifyDataSetChanged()
view_pager_bookshelf.adapter?.notifyDataSetChanged()
tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select()
tab_layout.addOnTabSelectedListener(this)
}
@ -149,7 +142,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
if (AppConfig.bookGroupAllShow) {
bookGroups.add(0, AppConst.bookGroupAll)
}
bookshelfAdapter.notifyDataSetChanged()
view_pager_bookshelf.adapter?.notifyDataSetChanged()
}
}
@ -212,4 +205,31 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
putPrefInt(PreferKey.saveTabPosition, it)
}
}
private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getPageTitle(position: Int): CharSequence? {
return bookGroups[position].groupName
}
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getItem(position: Int): Fragment {
val group = bookGroups[position]
var fragment = fragmentMap[group.groupId]
if (fragment == null) {
fragment = BooksFragment.newInstance(position, group.groupId)
fragmentMap[group.groupId] = fragment
}
return fragment
}
override fun getCount(): Int {
return bookGroups.size
}
}
}

@ -43,10 +43,8 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
)
WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) {
it?.let { book ->
App.db.bookDao().insert(book)
successCount++
}
App.db.bookDao().insert(it)
successCount++
}.onError {
throw Exception(it.localizedMessage)
}

@ -21,7 +21,6 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
if (bundle == null) {
ATH.applyBackgroundTint(this)
tv_name.text = item.name
bv_author.text = item.author
iv_cover.load(item.getDisplayCover(), item.name, item.author)
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
bv_unread.invisible()
@ -35,7 +34,6 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
bundle.keySet().map {
when (it) {
"name" -> tv_name.text = item.name
"author" -> bv_author.text = item.author
"cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author)
"refresh" -> if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
bv_unread.invisible()

@ -7,6 +7,7 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseFragment
@ -26,6 +27,7 @@ import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_books.*
import org.jetbrains.anko.startActivity
import kotlin.math.max
class BooksFragment : BaseFragment(R.layout.fragment_books),
@ -71,9 +73,26 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
booksAdapter = BooksAdapterList(requireContext(), this)
} else {
rv_bookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2)
booksAdapter = BooksAdapterGrid(requireContext(),this)
booksAdapter = BooksAdapterGrid(requireContext(), this)
}
rv_bookshelf.adapter = booksAdapter
booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
val layoutManager = rv_bookshelf.layoutManager
if (positionStart == 0 && layoutManager is LinearLayoutManager) {
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
rv_bookshelf.scrollToPosition(max(0, scrollTo))
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
val layoutManager = rv_bookshelf.layoutManager
if (toPosition == 0 && layoutManager is LinearLayoutManager) {
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
rv_bookshelf.scrollToPosition(max(0, scrollTo))
}
}
})
}
private fun upRecyclerData() {

@ -12,6 +12,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache
import io.legado.app.utils.dp
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.*
@ -23,16 +24,21 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) :
SimpleRecyclerAdapter<BookSource>(context, R.layout.item_find_book) {
private var exIndex = 0
private var exIndex = -1
private var scrollTo = -1
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) {
if (holder.layoutPosition == getActualItemCount() - 1) {
setPadding(16.dp, 12.dp, 16.dp, 12.dp)
} else {
setPadding(16.dp, 12.dp, 16.dp, 0)
}
if (payloads.isEmpty()) {
tv_name.text = item.bookSourceName
}
if (exIndex == holder.layoutPosition) {
iv_status.setImageResource(R.drawable.ic_remove)
iv_status.setImageResource(R.drawable.ic_arrow_down)
rotate_loading.loadingColor = context.accentColor
rotate_loading.show()
if (scrollTo >= 0) {
@ -68,7 +74,7 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
}
}
} else {
iv_status.setImageResource(R.drawable.ic_add)
iv_status.setImageResource(R.drawable.ic_arrow_right)
rotate_loading.hide()
gl_child.gone()
}

@ -24,6 +24,7 @@ import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.prefs.NameListPreference
import io.legado.app.ui.widget.prefs.PreferenceCategory
import io.legado.app.ui.widget.prefs.SwitchPreference
@ -48,9 +49,10 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.
override fun onCompatOptionsItemSelected(item: MenuItem) {
when (item.itemId) {
R.id.menu_help -> startActivity<AboutActivity>()
R.id.menu_backup -> BackupRestoreUi.backup(this)
R.id.menu_restore -> BackupRestoreUi.restore(this)
R.id.menu_help -> {
val text = String(requireContext().assets.open("help.md").readBytes())
TextDialog.show(childFragmentManager, text, TextDialog.MD)
}
}
}

@ -8,7 +8,6 @@ import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.ImportOldData
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers
@ -108,7 +107,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
FileUtils.createFileIfNotExist(file, "exportReplaceRule.json")
.writeText(json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${file.absolutePath}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}
@ -121,7 +120,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
doc.createFile("", "exportReplaceRule.json")
?.writeText(context, json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${doc.uri.path}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}

@ -44,8 +44,8 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
rssSource?.let { rssSource ->
Rss.getArticles(rssSource, null)
.onSuccess(IO) {
nextPageUrl = it?.nextPageUrl
it?.articles?.let { list ->
nextPageUrl = it.nextPageUrl
it.articles.let { list ->
list.forEach { rssArticle ->
rssArticle.order = order--
}
@ -76,8 +76,8 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
if (source != null && !pageUrl.isNullOrEmpty()) {
Rss.getArticles(source, pageUrl)
.onSuccess(IO) {
nextPageUrl = it?.nextPageUrl
it?.articles?.let { list ->
nextPageUrl = it.nextPageUrl
it.articles.let { list ->
if (list.isEmpty()) {
callBack?.loadFinally(false)
return@let

@ -22,7 +22,6 @@ import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.share
import org.jetbrains.anko.toast
import org.jsoup.Jsoup
import org.jsoup.safety.Whitelist
class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read),
@ -194,7 +193,7 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
}
override fun upStarMenu() {
if (viewModel.star) {
if (viewModel.rssStar != null) {
starMenuItem?.setIcon(R.drawable.ic_star)
starMenuItem?.setTitle(R.string.in_favorites)
} else {
@ -246,18 +245,15 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
@SuppressLint("SetJavaScriptEnabled")
private fun readAloud() {
if (viewModel.textToSpeech.isSpeaking) {
viewModel.textToSpeech.stop()
if (viewModel.textToSpeech?.isSpeaking == true) {
viewModel.textToSpeech?.stop()
upTtsMenu(false)
} else {
web_view.settings.javaScriptEnabled = true
web_view.evaluateJavascript("document.documentElement.outerHTML") {
val html = StringEscapeUtils.unescapeJson(it)
val text = Jsoup.clean(html, Whitelist.none())
.replace(Regex("""&\w+;"""), "")
.trim()//朗读过程中总是听到一些杂音,清理一下
//longToast(需读内容)调试一下
viewModel.readAloud(text)
Jsoup.parse(html).text()
viewModel.readAloud(Jsoup.parse(html).textArray())
}
}
}

@ -15,6 +15,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.data.entities.RssStar
import io.legado.app.help.http.HttpHelper
import io.legado.app.model.Rss
import io.legado.app.model.analyzeRule.AnalyzeUrl
@ -22,6 +23,7 @@ import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentPath
import io.legado.app.utils.writeBytes
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
@ -34,8 +36,10 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
var rssArticle: RssArticle? = null
val contentLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<AnalyzeUrl>()
var star = false
var textToSpeech: TextToSpeech = TextToSpeech(context, this)
var rssStar: RssStar? = null
var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
private var ttsTextList = arrayListOf<String>()
fun initData(intent: Intent) {
execute {
@ -43,8 +47,8 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
val link = intent.getStringExtra("link")
if (origin != null && link != null) {
rssSource = App.db.rssSourceDao().getByKey(origin)
star = App.db.rssStarDao().get(origin, link) != null
rssArticle = App.db.rssArticleDao().get(origin, link)
rssStar = App.db.rssStarDao().get(origin, link)
rssArticle = rssStar?.toRssArticle() ?: App.db.rssArticleDao().get(origin, link)
rssArticle?.let { rssArticle ->
if (!rssArticle.description.isNullOrBlank()) {
contentLiveData.postValue(rssArticle.description)
@ -76,21 +80,26 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
}
private fun loadContent(rssArticle: RssArticle, ruleContent: String) {
Rss.getContent(rssArticle, ruleContent, this)
.onSuccess {
contentLiveData.postValue(it)
Rss.getContent(rssArticle, ruleContent, rssSource, this)
.onSuccess(IO) { body ->
rssArticle.description = body
App.db.rssArticleDao().insert(rssArticle)
rssStar?.let {
it.description = body
App.db.rssStarDao().insert(it)
}
contentLiveData.postValue(body)
}
}
fun favorite() {
execute {
rssArticle?.let {
if (star) {
App.db.rssStarDao().delete(it.origin, it.link)
} else {
App.db.rssStarDao().insert(it.toStar())
}
star = !star
rssStar?.let {
App.db.rssStarDao().delete(it.origin, it.link)
rssStar = null
} ?: rssArticle?.toStar()?.let {
App.db.rssStarDao().insert(it)
rssStar = it
}
}.onSuccess {
callBack?.upStarMenu()
@ -143,28 +152,43 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
}
}
@Synchronized
override fun onInit(status: Int) {
launch {
if (status == TextToSpeech.SUCCESS) {
textToSpeech.language = Locale.CHINA
textToSpeech.setOnUtteranceProgressListener(TTSUtteranceListener())
} else {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsInitFinish = true
play()
} else {
launch {
toast(R.string.tts_init_failed)
}
}
}
fun readAloud(text: String) {
textToSpeech.stop()
text.split("\n", " ", "  ").forEach {
textToSpeech.speak(it, TextToSpeech.QUEUE_ADD, null, "rss")
@Synchronized
private fun play() {
if (!ttsInitFinish) return
textToSpeech?.stop()
ttsTextList.forEach {
textToSpeech?.speak(it, TextToSpeech.QUEUE_ADD, null, "rss")
}
}
fun readAloud(textArray: Array<String>) {
ttsTextList.clear()
ttsTextList.addAll(textArray)
textToSpeech?.let {
play()
} ?: let {
textToSpeech = TextToSpeech(context, this)
}
}
override fun onCleared() {
super.onCleared()
textToSpeech.stop()
textToSpeech.shutdown()
textToSpeech?.stop()
textToSpeech?.shutdown()
}
/**

@ -0,0 +1,17 @@
package io.legado.app.ui.rss.read
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
class VisibleWebView(
context: Context,
attrs: AttributeSet? = null
) : WebView(context, attrs) {
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(View.VISIBLE)
}
}

@ -24,10 +24,7 @@ import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -68,6 +65,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() }
App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>()

@ -9,7 +9,6 @@ import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.*
import org.jetbrains.anko.toast
@ -74,7 +73,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
FileUtils.createFileIfNotExist(file, "exportRssSource.json")
.writeText(json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${file.absolutePath}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}
@ -87,7 +86,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
doc.createFile("", "exportRssSource.json")
?.writeText(context, json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
context.toast("成功导出至\n${doc.uri.path}")
}.onError {
context.toast("导出失败\n${it.localizedMessage}")
}
@ -187,7 +186,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
}.onError {
finally(it.localizedMessage ?: "")
}.onSuccess {
finally(it ?: "导入完成")
finally(it)
}
}

@ -58,4 +58,7 @@ open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) {
class Launcher1 : WelcomeActivity()
class Launcher2 : WelcomeActivity()
class Launcher3 : WelcomeActivity()
class Launcher3 : WelcomeActivity()
class Launcher4 : WelcomeActivity()
class Launcher5 : WelcomeActivity()
class Launcher6 : WelcomeActivity()

@ -0,0 +1,110 @@
package io.legado.app.ui.widget
import android.app.SearchableInfo
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import io.legado.app.R
class SearchView : SearchView {
private var mSearchHintIcon: Drawable? = null
private var textView: TextView? = null
constructor(
context: Context?,
attrs: AttributeSet? = null
) : super(context, attrs)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
try {
if (textView == null) {
textView = findViewById(androidx.appcompat.R.id.search_src_text)
mSearchHintIcon = this.context.getDrawable(R.drawable.ic_search_hint)
updateQueryHint()
}
// 改变字体
textView!!.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
textView!!.gravity = Gravity.CENTER_VERTICAL
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getDecoratedHint(hintText: CharSequence): CharSequence {
// If the field is always expanded or we don't have a search hint icon,
// then don't add the search icon to the hint.
if (mSearchHintIcon == null) {
return hintText
}
val textSize = (textView!!.textSize * 0.8).toInt()
mSearchHintIcon!!.setBounds(0, 0, textSize, textSize)
val ssb = SpannableStringBuilder(" ")
ssb.setSpan(CenteredImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
ssb.append(hintText)
return ssb
}
private fun updateQueryHint() {
textView?.let {
it.hint = getDecoratedHint(queryHint ?: "")
}
}
override fun setIconifiedByDefault(iconified: Boolean) {
super.setIconifiedByDefault(iconified)
updateQueryHint()
}
override fun setSearchableInfo(searchable: SearchableInfo?) {
super.setSearchableInfo(searchable)
searchable?.let {
updateQueryHint()
}
}
override fun setQueryHint(hint: CharSequence?) {
super.setQueryHint(hint)
updateQueryHint()
}
internal class CenteredImageSpan(drawable: Drawable?) : ImageSpan(drawable!!) {
override fun draw(
canvas: Canvas, text: CharSequence,
start: Int, end: Int, x: Float,
top: Int, y: Int, bottom: Int, paint: Paint
) {
// image to draw
val b = drawable
// font metrics of text to be replaced
val fm = paint.fontMetricsInt
val transY = ((y + fm.descent + y + fm.ascent) / 2
- b.bounds.bottom / 2)
canvas.save()
canvas.translate(x, transY.toFloat())
b.draw(canvas)
canvas.restore()
}
}
}

@ -0,0 +1,185 @@
package io.legado.app.ui.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import io.legado.app.R
import io.legado.app.utils.getCompatColor
/**
* ShadowLayout.java
*
*
* Created by lijiankun on 17/8/11.
*/
class ShadowLayout(
context: Context,
attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val mPaint =
Paint(Paint.ANTI_ALIAS_FLAG)
private val mRectF = RectF()
/**
* 阴影的颜色
*/
private var mShadowColor = Color.TRANSPARENT
/**
* 阴影的大小范围
*/
private var mShadowRadius = 0f
/**
* 阴影 x 轴的偏移量
*/
private var mShadowDx = 0f
/**
* 阴影 y 轴的偏移量
*/
private var mShadowDy = 0f
/**
* 阴影显示的边界
*/
private var mShadowSide = ALL
/**
* 阴影的形状圆形/矩形
*/
private var mShadowShape = SHAPE_RECTANGLE
init {
setLayerType(View.LAYER_TYPE_SOFTWARE, null) // 关闭硬件加速
setWillNotDraw(false) // 调用此方法后,才会执行 onDraw(Canvas) 方法
val typedArray =
context.obtainStyledAttributes(attrs, R.styleable.ShadowLayout)
mShadowColor = typedArray.getColor(
R.styleable.ShadowLayout_shadowColor,
context.getCompatColor(android.R.color.black)
)
mShadowRadius =
typedArray.getDimension(R.styleable.ShadowLayout_shadowRadius, dip2px(0f))
mShadowDx = typedArray.getDimension(R.styleable.ShadowLayout_shadowDx, dip2px(0f))
mShadowDy = typedArray.getDimension(R.styleable.ShadowLayout_shadowDy, dip2px(0f))
mShadowSide =
typedArray.getInt(R.styleable.ShadowLayout_shadowSide, ALL)
mShadowShape = typedArray.getInt(
R.styleable.ShadowLayout_shadowShape,
SHAPE_RECTANGLE
)
typedArray.recycle()
setUpShadowPaint()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val effect = mShadowRadius + dip2px(5f)
var rectLeft = 0f
var rectTop = 0f
var rectRight = this.measuredWidth.toFloat()
var rectBottom = this.measuredHeight.toFloat()
var paddingLeft = 0
var paddingTop = 0
var paddingRight = 0
var paddingBottom = 0
this.width
if (mShadowSide and LEFT == LEFT) {
rectLeft = effect
paddingLeft = effect.toInt()
}
if (mShadowSide and TOP == TOP) {
rectTop = effect
paddingTop = effect.toInt()
}
if (mShadowSide and RIGHT == RIGHT) {
rectRight = this.measuredWidth - effect
paddingRight = effect.toInt()
}
if (mShadowSide and BOTTOM == BOTTOM) {
rectBottom = this.measuredHeight - effect
paddingBottom = effect.toInt()
}
if (mShadowDy != 0.0f) {
rectBottom -= mShadowDy
paddingBottom += mShadowDy.toInt()
}
if (mShadowDx != 0.0f) {
rectRight -= mShadowDx
paddingRight += mShadowDx.toInt()
}
mRectF.left = rectLeft
mRectF.top = rectTop
mRectF.right = rectRight
mRectF.bottom = rectBottom
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
}
/**
* 真正绘制阴影的方法
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
setUpShadowPaint()
if (mShadowShape == SHAPE_RECTANGLE) {
canvas.drawRect(mRectF, mPaint)
} else if (mShadowShape == SHAPE_OVAL) {
canvas.drawCircle(
mRectF.centerX(),
mRectF.centerY(),
mRectF.width().coerceAtMost(mRectF.height()) / 2,
mPaint
)
}
}
fun setShadowColor(shadowColor: Int) {
mShadowColor = shadowColor
requestLayout()
postInvalidate()
}
fun setShadowRadius(shadowRadius: Float) {
mShadowRadius = shadowRadius
requestLayout()
postInvalidate()
}
private fun setUpShadowPaint() {
mPaint.reset()
mPaint.isAntiAlias = true
mPaint.color = Color.TRANSPARENT
mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor)
}
/**
* dip2px dp 值转 px
*
* @param dpValue dp
* @return px
*/
private fun dip2px(dpValue: Float): Float {
val dm = context.resources.displayMetrics
val scale = dm.density
return dpValue * scale + 0.5f
}
companion object {
const val ALL = 0x1111
const val LEFT = 0x0001
const val TOP = 0x0010
const val RIGHT = 0x0100
const val BOTTOM = 0x1000
const val SHAPE_RECTANGLE = 0x0001
const val SHAPE_OVAL = 0x0010
}
}

@ -2,6 +2,7 @@ package io.legado.app.ui.widget
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.AttributeSet
import android.view.Menu
import android.view.View
@ -186,6 +187,11 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a
toolbar.setSubtitleTextAppearance(context, resId)
}
fun transparent() {
elevation = 0f
backgroundColor = Color.TRANSPARENT
}
private fun attachToActivity() {
if (attachToActivity) {
activity?.let {

@ -5,29 +5,40 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import kotlinx.android.synthetic.main.dialog_text_view.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.noties.markwon.Markwon
class TextDialog : DialogFragment() {
class TextDialog : BaseDialogFragment() {
companion object {
const val MD = 1
fun show(fragmentManager: FragmentManager, content: String?, mode: Int = 0) {
fun show(
fragmentManager: FragmentManager,
content: String?,
mode: Int = 0,
time: Long = 0
) {
TextDialog().apply {
val bundle = Bundle()
bundle.putString("content", content)
bundle.putInt("mode", mode)
bundle.putLong("time", time)
arguments = bundle
isCancelable = false
}.show(fragmentManager, "textDialog")
}
}
private var time = 0L
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
@ -43,8 +54,7 @@ class TextDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_text_view, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
arguments?.let {
val content = it.getString("content") ?: ""
when (it.getInt("mode")) {
@ -57,8 +67,27 @@ class TextDialog : DialogFragment() {
}
else -> text_view.text = content
}
time = it.getLong("time", 0L)
}
if (time > 0) {
badge_view.setBadgeCount((time / 1000).toInt())
launch {
while (time > 0) {
delay(1000)
time -= 1000
badge_view.setBadgeCount((time / 1000).toInt())
if (time <= 0) {
view.post {
dialog?.setCancelable(true)
}
}
}
}
} else {
view.post {
dialog?.setCancelable(true)
}
}
}
}

@ -37,6 +37,8 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
init {
isPersistent = true
layoutResource = io.legado.app.R.layout.view_preference
val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference)
showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true)
@ -109,9 +111,12 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
val v = io.legado.app.ui.widget.prefs.Preference.bindView<ColorPanelView>(context, holder, icon, title, summary, widgetLayoutResource,
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30)
if (v is ColorPanelView) {
v.color = color
}
super.onBindViewHolder(holder)
val preview = holder.itemView.findViewById(R.id.cpv_preference_preview_color_panel) as ColorPanelView
preview.color = color
}
override fun onSetInitialValue(defaultValue: Any?) {

@ -0,0 +1,21 @@
package io.legado.app.ui.widget.prefs
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import io.legado.app.R
class EditTextPreference(context: Context, attrs: AttributeSet) : androidx.preference.EditTextPreference(context, attrs) {
init {
// isPersistent = true
layoutResource = R.layout.view_preference
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
super.onBindViewHolder(holder)
}
}

@ -29,6 +29,7 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
private val mEntryDrawables = arrayListOf<Drawable?>()
init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.view_icon
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0)
@ -52,11 +53,12 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
holder?.itemView?.findViewById<ImageView>(R.id.preview)?.let {
val v = Preference.bindView<ImageView>(context, holder, icon, title, summary, widgetLayoutResource, R.id.preview, 50, 50)
if (v is ImageView) {
val selectedIndex = findIndexOfValue(value)
if (selectedIndex >= 0) {
val drawable = mEntryDrawables[selectedIndex]
it.setImageDrawable(drawable)
v.setImageDrawable(drawable)
}
}
}

@ -11,12 +11,15 @@ import io.legado.app.R
class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) {
init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.item_fillet_text
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
val v = Preference.bindView<TextView>(context, holder, icon, title, summary, widgetLayoutResource, R.id.text_view)
if (v is TextView) {
v.text = entry
}
super.onBindViewHolder(holder)
val textView = holder?.itemView?.findViewById<TextView>(R.id.text_view)
textView?.text = entry
}
}

@ -0,0 +1,85 @@
package io.legado.app.ui.widget.prefs
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceViewHolder
import io.legado.app.R
import io.legado.app.lib.theme.accentColor
import org.jetbrains.anko.layoutInflater
import kotlin.math.roundToInt
class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Preference(context, attrs) {
init {
// isPersistent = true
layoutResource = R.layout.view_preference
}
companion object {
fun <T: View> bindView(context: Context, it: PreferenceViewHolder?, icon: Drawable?, title: CharSequence?, summary: CharSequence?, weightLayoutRes: Int?, viewId: Int?,
weightWidth: Int = 0, weightHeight: Int = 0): T? {
if (it == null) return null
val view = it.findViewById(R.id.preference_title)
if (view is TextView) {
view.text = title
view.isVisible = title != null && title.isNotEmpty()
val tvSummary = it.findViewById(R.id.preference_desc)
if (tvSummary is TextView) {
tvSummary.text = summary
tvSummary.isVisible = summary != null && summary.isNotEmpty()
}
val iconView = it.findViewById(R.id.preference_icon)
if (iconView is ImageView) {
iconView.isVisible = icon != null && icon.isVisible
iconView.setImageDrawable(icon)
iconView.setColorFilter(context.accentColor)
}
}
if (weightLayoutRes != null && weightLayoutRes != 0 && viewId != null && viewId != 0) {
val lay = it.findViewById(R.id.preference_widget)
if (lay is FrameLayout) {
var v = it.itemView.findViewById<T>(viewId)
if (v == null) {
val inflater: LayoutInflater = context.layoutInflater
val childView = inflater.inflate(weightLayoutRes, null)
lay.removeAllViews()
lay.addView(childView)
lay.isVisible = true
v = lay.findViewById(viewId)
}
if (weightWidth > 0 || weightHeight > 0) {
val lp = lay.layoutParams
if (weightHeight > 0)
lp.height = (context.resources.displayMetrics.density * weightHeight).roundToInt()
if (weightWidth > 0)
lp.width = (context.resources.displayMetrics.density * weightWidth).roundToInt()
lay.layoutParams = lp
}
return v
}
}
return null
}
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
bindView<View>(context, holder, icon, title, summary, null, null)
super.onBindViewHolder(holder)
}
}

@ -1,22 +1,41 @@
package io.legado.app.ui.widget.prefs
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceViewHolder
import io.legado.app.R
import io.legado.app.lib.theme.accentColor
class PreferenceCategory(context: Context, attrs: AttributeSet) :
PreferenceCategory(context, attrs) {
class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) {
init {
isPersistent = true
layoutResource = R.layout.view_preference_category
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
holder?.let {
val view = it.findViewById(android.R.id.title)
if (view is TextView && !view.isInEditMode) {
view.setTextColor(context.accentColor)//设置title文本的颜色
val view = it.findViewById(R.id.preference_title)
if (view is TextView) { // && !view.isInEditMode
view.text = title
view.setTextColor(context.accentColor) //设置title文本的颜色
view.isVisible = title != null && title.isNotEmpty()
val da = it.findViewById(R.id.preference_divider_above)
if (da is View) {
da.isVisible = it.isDividerAllowedAbove
}
val db = it.findViewById(R.id.preference_divider_below)
if (db is View) {
db.isVisible = it.isDividerAllowedBelow
}
}
}
}

@ -12,14 +12,24 @@ import io.legado.app.lib.theme.accentColor
class SwitchPreference(context: Context, attrs: AttributeSet) :
SwitchPreferenceCompat(context, attrs) {
init {
layoutResource = R.layout.view_preference
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
holder?.let {
val view = it.findViewById(R.id.switchWidget)
if (view is SwitchCompat) {
ATH.setTint(view, context.accentColor)
}
val v = Preference.bindView<SwitchCompat>(
context,
holder,
icon,
title,
summary,
widgetLayoutResource,
R.id.switchWidget
)
if (v is SwitchCompat) {
ATH.setTint(v, context.accentColor)
}
super.onBindViewHolder(holder)
}
}

@ -98,7 +98,7 @@ object DocumentUtils {
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_MIME_TYPE
), null, null, null
), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME
)
c?.let {
val ici = c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)

@ -0,0 +1,66 @@
package io.legado.app.utils
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.CDataNode
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import org.jsoup.select.NodeTraversor
import org.jsoup.select.NodeVisitor
fun Element.textArray(): Array<String> {
val accum = StringUtil.borrowBuilder()
NodeTraversor.traverse(object : NodeVisitor {
override fun head(node: Node, depth: Int) {
if (node is TextNode) {
appendNormalisedText(accum, node)
} else if (node is Element) {
if (accum.isNotEmpty() &&
(node.isBlock || node.tag().name == "br") &&
!lastCharIsWhitespace(accum)
) accum.append("\n")
}
}
override fun tail(node: Node, depth: Int) {
if (node is Element) {
if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace(
accum
)
) accum.append("\n")
}
}
}, this)
val text = StringUtil.releaseBuilder(accum).trim { it <= ' ' }
return text.splitNotBlank("\n")
}
private fun appendNormalisedText(accum: StringBuilder, textNode: TextNode) {
val text = textNode.wholeText
if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode)
accum.append(text)
else StringUtil.appendNormalisedWhitespace(
accum,
text,
lastCharIsWhitespace(accum)
)
}
private fun preserveWhitespace(node: Node?): Boolean {
if (node is Element) {
var el = node as Element?
var i = 0
do {
if (el!!.tag().preserveWhitespace()) return true
el = el.parent()
i++
} while (i < 6 && el != null)
}
return false
}
private fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean {
return sb.isNotEmpty() && sb[sb.length - 1] == ' '
}

@ -6,6 +6,7 @@ import android.graphics.Canvas
import android.os.Build
import android.view.View
import android.view.View.*
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.RadioGroup
import android.widget.SeekBar
@ -75,6 +76,13 @@ fun View.screenshot(): Bitmap? {
}.getOrNull()
}
fun View.setMargin(left: Int, top: Int, right: Int, bottom: Int) {
if (layoutParams is ViewGroup.MarginLayoutParams) {
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(left, top, right, bottom)
requestLayout()
}
}
fun SeekBar.progressAdd(int: Int) {
progress += int
}

@ -1,24 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M34.73,33.55l-0.09,13.54A5.17,5.17 0,0 1,33.23 50c-0.23,0.06 -0.38,0 -0.44,-0.18l0.09,-6.41L33,34.25l-0.09,-2.55 2,0.71c0.18,0.06 0.21,0.17 0.09,0.35ZM43.08,40.41c0.11,0.06 0.11,0.14 0,0.26l-0.27,0.53 -0.52,2.55c-0.12,0.47 0,0.73 0.35,0.79a4.7,4.7 0,0 0,1.84 -0.18c0.41,0 0.74,-0.38 1,-1.14a0.26,0.26 0,0 1,0.18 -0.09s0.08,0.06 0.08,0.18c-0.11,0.7 0,1.11 0.27,1.23 0.64,0.76 0.44,1.32 -0.62,1.67a6,6 0,0 1,-2.72 0.17c-1.06,-0.29 -1.56,-1.23 -1.5,-2.81l0.09,-2.2L41.26,39.79L39.47,39.79l1.23,0.53c0.12,0.06 0.12,0.15 0,0.26l-0.26,0.35a19.72,19.72 0,0 1,-5.62 7c-0.18,0.05 -0.21,0 -0.09,-0.18a19.79,19.79 0,0 0,4.22 -8h-0.71a3.35,3.35 0,0 1,-1.05 1.41c-0.3,0.17 -0.47,0.14 -0.53,-0.09l0.09,-2.46L36.75,37.42q0,-0.44 -0.09,-1.41v-0.79q0,-0.09 0.18,-0.09l1.49,0.79 2.9,-0.08a16,16 0,0 0,1.14 -4l1.94,0.62c0.11,0 0.11,0.17 0,0.35s-0.5,0.52 -1,1l-1.76,1.94L43,35.75l0.79,-1 1.76,1c0.11,0 0.11,0 0,0.17l-0.44,0.7L45,39.35l0.18,0.27c0,0.11 0,0.17 -0.09,0.17L41.76,39.79ZM35.17,28.72a0.87,0.87 0,0 1,0.26 0.08c1.93,0.65 2.87,1.18 2.81,1.59s-0.26,1 -1,1.67c-0.41,0.17 -0.68,0.08 -0.79,-0.27a8,8 0,0 0,-1.5 -2.9C34.93,28.72 35,28.66 35.17,28.72ZM37.89,32.41c1.47,0.64 2.23,1.23 2.29,1.76a1.51,1.51 0,0 1,-0.88 1.23c-0.35,0.06 -0.56,-0.06 -0.62,-0.36a3.14,3.14 0,0 0,-0.88 -2.46C37.74,32.41 37.77,32.35 37.89,32.41ZM38.24,36.41v3L43,39.26v-3ZM42.46,31.41a20,20 0,0 0,-2.72 0.18L39,31l7.65,-0.26 1,-1.06 1.67,1.15h0.09c0.05,0.05 0,0.14 -0.09,0.26l-0.62,1 0.09,8.79L49,45.68a4.38,4.38 0,0 1,-2.73 4.13,0.66 0.66,0 0,1 -0.26,0.09c-0.06,-0.06 -0.06,-0.15 0,-0.26a2.43,2.43 0,0 0,-0.88 -1.85,0.26 0.26,0 0,1 -0.09,-0.18c0,-0.06 0,-0.08 0.09,-0.08 1.23,0 1.79,-0.24 1.67,-0.88l-0.09,-15.47Z"
android:fillColor="#fff" />
<path
android:pathData="M63.27,43.66c0.18,-0.06 0.21,0 0.09,0.17L60.28,47a21.87,21.87 0,0 0,-1.67 1.67c-0.06,0.12 -0.14,0.12 -0.26,0L57,47.35c-0.17,-0.12 -0.2,-0.2 -0.09,-0.26 0.82,-0.41 1.44,-0.71 1.85,-0.88l-0.09,-10 -1.49,0.09a6.65,6.65 0,0 0,-1.76 0.17l-0.79,-0.7 3.78,-0.09 1,-1.14 1.75,1q0.18,0.09 0,0.27l-0.44,0.61 -0.61,9ZM57.27,29.24c1.69,0.65 2.69,1.26 3,1.85a1.93,1.93 0,0 1,-1 1.85,0.6 0.6,0 0,1 -0.88,-0.27 6.33,6.33 0,0 0,-1.32 -3.25C57.06,29.24 57.12,29.19 57.3,29.24ZM67.84,36.1l2.38,0.79c0.17,0.06 0.2,0.21 0.08,0.44l-0.43,0.62 -0.27,2.9a9.75,9.75 0,0 1,-0.26 1.4l3.6,-0.09 1.23,-1.4 1.76,1.58q0.18,0.18 -0.09,0.18l-6.59,0.26q-0.61,5.1 -11.51,7.56 -0.27,0 -0.27,-0.09a0.28,0.28 0,0 1,0.18 -0.26q9,-2.46 9.75,-7.21L64.77,43a13,13 0,0 0,-2 0.17,1.48 1.48,0 0,1 -0.44,0.09l-0.53,-0.7 5.71,-0.18A5.35,5.35 0,0 0,67.67 41l0.17,-1.76a18,18 0,0 0,0 -2.55ZM65.91,32.58c-0.82,0.06 -1.58,0.15 -2.29,0.27l-0.7,-0.71 4.31,-0.08v-0.62c0,-1.17 0,-2.08 -0.09,-2.72l2.37,0.61q0.18,0 0.18,0.09a0.28,0.28 0,0 1,-0.09 0.35,1.9 1.9,0 0,0 -0.35,1.32l-0.09,0.88 2.29,-0.09 1,-1.05 1.41,1.23 0.09,0.17a0.26,0.26 0,0 1,-0.18 0.09l-4.66,0.18v2.63L72.68,35l0.88,-1 2.11,1.84a0.29,0.29 0,0 1,0.17 0.27,0.9 0.9,0 0,1 -0.26,0.17 5.83,5.83 0,0 0,-3.34 1.41q-0.27,0.09 -0.18,-0.18l0.62,-2 -7.3,0.18a7.26,7.26 0,0 0,-0.79 0.09c-0.94,0.11 -1.55,0.2 -1.84,0.26L62,35.4l5.19,-0.18L67.19,32.58ZM62.83,38.74c1.82,0.47 2.79,0.93 2.9,1.4a1.47,1.47 0,0 1,-0.52 1.41c-0.36,0.29 -0.62,0.23 -0.79,-0.18A8.87,8.87 0,0 0,62.75 39C62.63,38.82 62.66,38.74 62.83,38.74ZM64.15,36.36c1.58,0.35 2.4,0.71 2.46,1.06a1.54,1.54 0,0 1,-0.44 1.32c-0.35,0.23 -0.58,0.17 -0.7,-0.18a3.66,3.66 0,0 0,-1.32 -2Q63.9,36.45 64.15,36.36ZM69.15,44.36q3.87,1.23 4.92,2.46c0.24,0.47 0.09,1.15 -0.44,2 -0.23,0.24 -0.41,0.36 -0.52,0.36s-0.27,-0.15 -0.44,-0.44a11.71,11.71 0,0 0,-3.61 -4.22Q69,44.45 69.16,44.36Z"
android:fillColor="#fff" />
<path
android:pathData="M44.5,53.5l0,0 0,0a2,2 0,0 1,0.57 -0.33,2.29 2.29,0 0,1 0.69,-0.14v0.49a1.39,1.39 0,0 0,-0.5 0.09,1.27 1.27,0 0,0 -0.42,0.25 1.23,1.23 0,0 0,-0.29 0.35,1.33 1.33,0 0,0 -0.14,0.44v2.16H44V53h0.52Z"
android:fillColor="#fff" />
<path
android:pathData="M51.49,56.35a2.72,2.72 0,0 1,-0.71 0.29,2.57 2.57,0 0,1 -0.72,0.11 2.42,2.42 0,0 1,-1 -0.2,2.22 2.22,0 0,1 -0.42,-0.25 1.88,1.88 0,0 1,-0.34 -0.35,1.54 1.54,0 0,1 -0.24,-0.48 2,2 0,0 1,-0.09 -0.61,2 2,0 0,1 0.09,-0.62 1.69,1.69 0,0 1,0.23 -0.51,1.86 1.86,0 0,1 0.36,-0.38 2.43,2.43 0,0 1,0.44 -0.26,2.06 2.06,0 0,1 0.76,-0.17 1.92,1.92 0,0 1,0.74 0.15,2.22 2.22,0 0,1 0.42,0.25 1.48,1.48 0,0 1,0.33 0.37,1.76 1.76,0 0,1 0.22,0.48 2.23,2.23 0,0 1,0.08 0.6L51.64,55L48.51,55a1.41,1.41 0,0 0,0.09 0.38,1.29 1.29,0 0,0 0.16,0.31 1.47,1.47 0,0 0,0.55 0.44,2 2,0 0,0 0.75,0.14 2.36,2.36 0,0 0,0.58 -0.07,2.31 2.31,0 0,0 0.57,-0.24ZM49.33,53.51a1.51,1.51 0,0 0,-0.5 0.38,1.4 1.4,0 0,0 -0.29,0.64h2.58a1.3,1.3 0,0 0,-0.24 -0.66,1.35 1.35,0 0,0 -0.49,-0.37 1.27,1.27 0,0 0,-0.52 -0.1A1.37,1.37 0,0 0,49.33 53.51Z"
android:fillColor="#fff" />
<path
android:pathData="M53.9,53.46a1.72,1.72 0,0 1,0.53 -0.34,2.83 2.83,0 0,1 0.5,-0.13 2.45,2.45 0,0 1,0.54 0,1.86 1.86,0 0,1 0.52,0.11 1.13,1.13 0,0 1,0.43 0.26,0.89 0.89,0 0,1 0.25,0.34 1.4,1.4 0,0 1,0.14 0.56c0,0.2 0,0.43 0,0.68h0v1.76h-0.51v-0.34a1.82,1.82 0,0 1,-0.36 0.2,2.35 2.35,0 0,1 -0.37,0.14 1.71,1.71 0,0 1,-0.84 0,1.09 1.09,0 0,1 -0.83,-0.73 1.17,1.17 0,0 1,-0.05 -0.38,1 1,0 0,1 0.08,-0.42 1.13,1.13 0,0 1,0.24 -0.36,1.31 1.31,0 0,1 0.28,-0.2l0.32,-0.11 0.34,-0.05 0.38,0 0.39,0a2.7,2.7 0,0 0,0.41 -0.06,1.44 1.44,0 0,0 -0.1,-0.48 0.72,0.72 0,0 0,-0.14 -0.19,0.85 0.85,0 0,0 -0.28,-0.16 1.53,1.53 0,0 0,-0.36 -0.07,1.8 1.8,0 0,0 -0.8,0.13 0.75,0.75 0,0 0,-0.2 0.1,0.76 0.76,0 0,0 -0.17,0.13ZM56.32,54.92h0v-0.1l-0.41,0 -0.38,0 -0.56,0a0.82,0.82 0,0 0,-0.42 0.19,0.53 0.53,0 0,0 -0.12,0.2 0.49,0.49 0,0 0,0 0.25,0.69 0.69,0 0,0 0.13,0.43 0.61,0.61 0,0 0,0.36 0.23,1.18 1.18,0 0,0 0.59,0 2,2 0,0 0,0.43 -0.19,2.15 2.15,0 0,0 0.43,-0.33Z"
android:fillColor="#fff" />
<path
android:pathData="M62,56.74h-0.9a1.67,1.67 0,0 1,-0.71 -0.15,1.79 1.79,0 0,1 -0.56,-0.42 2,2 0,0 1,-0.36 -0.6,1.92 1.92,0 0,1 -0.13,-0.72 2,2 0,0 1,0.49 -1.34,1.76 1.76,0 0,1 0.56,-0.41 1.67,1.67 0,0 1,0.71 -0.15L62,52.95v-1.4h0.51v5.19L62,56.74ZM62,53.43h-0.9a1.12,1.12 0,0 0,-0.48 0.1,1.24 1.24,0 0,0 -0.39,0.29 1.29,1.29 0,0 0,-0.27 0.45,1.57 1.57,0 0,0 -0.1,0.58 1.47,1.47 0,0 0,0.1 0.56,1.29 1.29,0 0,0 0.27,0.45 1.11,1.11 0,0 0,0.39 0.29,1.13 1.13,0 0,0 0.48,0.11L62,56.26Z"
android:fillColor="#fff" />
</vector>

@ -1,56 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="#4d4d4d" />
<path android:pathData="M0,108H54V64c0,-12 -54,-5 -54,-5Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="79.39"
android:startX="0.55"
android:endY="88.08"
android:endX="53.13"
android:type="linear">
<item
android:offset="0"
android:color="#FFFFFFFF" />
<item
android:offset="1"
android:color="#FFCCCCCC" />
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M108,108H54V64c0,-12 54,-5 54,-5Z"
android:fillColor="#fff" />
<path android:pathData="M71.5,70 L78,76V57a94.89,94.89 0,0 0,-13 0.75V76Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="57.11"
android:startX="71.5"
android:endY="76"
android:endX="71.5"
android:type="linear">
<item
android:offset="0"
android:color="#FFFFFFFF" />
<item
android:offset="0.24"
android:color="#FFFFE1DD" />
<item
android:offset="0.58"
android:color="#FFFFBEB6" />
<item
android:offset="0.84"
android:color="#FFFFA89D" />
<item
android:offset="1"
android:color="#FFFFA094" />
</gradient>
</aapt:attr>
</path>
</vector>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 角度 -->
<corners android:radius="6dp"/>
<!-- 填充色 -->
<solid android:color="@color/transparent10" />
</shape>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/btn_bg_press">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/background_prefs" />
<corners android:radius="0dp" />
</shape>
</item>
</ripple>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 角度 -->
<corners android:radius="35dp"/>
<!-- 填充色 -->
<solid android:color="@color/transparent10" />
<!-- 描边 设置线宽及颜色 -->
<stroke
android:color="@color/transparent10"
android:width="0.5dp" />
</shape>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512,685.25l-278.62,-278.62 45.25,-45.25L512,594.75l233.38,-233.38 45.25,45.25z"
android:fillColor="#181818"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M434.94,790.62l-45.25,-45.25L623.04,512l-233.38,-233.38 45.25,-45.25L713.57,512z"
android:fillColor="#181818"/>
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_bottom_books_e" android:state_checked="false" />
<item android:drawable="@drawable/ic_bottom_books_s" android:state_checked="true" />
</selector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M23,3V2.5a0.5,0.5 0,0 0,-0.5 -0.5H5.69A4.63,4.63 0,0 0,1 6.06,4.51 4.51,0 0,0 5.5,11h17a0.5,0.5 0,0 0,0.5 -0.5V10a0.5,0.5 0,0 0,-0.41 -0.5,3 3,0 0,1 0,-5.94A0.5,0.5 0,0 0,23 3ZM2.56,5.9A3.09,3.09 0,0 1,5.62 3.5H19.67a4.43,4.43 0,0 0,0 6H5.5A3,3 0,0 1,2.56 5.9Z"
android:fillColor="#2f45a6" />
<path
android:pathData="M1,21v0.53a0.5,0.5 0,0 0,0.5 0.5H18.31A4.63,4.63 0,0 0,23 17.94,4.51 4.51,0 0,0 18.5,13H1.5a0.5,0.5 0,0 0,-0.5 0.5V14a0.5,0.5 0,0 0,0.41 0.5,3 3,0 0,1 0,5.94A0.5,0.5 0,0 0,1 21ZM21.44,18.1a3.09,3.09 0,0 1,-3.06 2.4h-14a4.43,4.43 0,0 0,0 -6H18.5A3,3 0,0 1,21.44 18.1Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,6.5a3,3 0,0 1,2.59 -3A0.5,0.5 0,0 0,23 3V2.5a0.5,0.5 0,0 0,-0.5 -0.5H5.69A4.63,4.63 0,0 0,1 6.06,4.51 4.51,0 0,0 5.5,11h17a0.5,0.5 0,0 0,0.5 -0.5V10a0.5,0.5 0,0 0,-0.41 -0.5A3,3 0,0 1,20 6.5Z"
android:fillColor="#2f45a6" />
<path
android:pathData="M4,17.5a3,3 0,0 1,-2.59 3A0.5,0.5 0,0 0,1 21v0.53a0.5,0.5 0,0 0,0.5 0.5H18.31A4.63,4.63 0,0 0,23 17.94,4.51 4.51,0 0,0 18.5,13H1.5a0.5,0.5 0,0 0,-0.5 0.5V14a0.5,0.5 0,0 0,0.41 0.5A3,3 0,0 1,4 17.5Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_bottom_explore_e" android:state_checked="false" />
<item android:drawable="@drawable/ic_bottom_explore_s" android:state_checked="true" />
</selector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2.5A9.5,9.5 0,1 1,2.5 12,9.51 9.51,0 0,1 12,2.5M12,1A11,11 0,1 0,23 12,11 11,0 0,0 12,1Z"
android:fillColor="#2f45a6" />
<path
android:pathData="M13.93,10.07l-1.29,2.57 -2.57,1.29 1.29,-2.57 2.57,-1.29M16.85,7a0.18,0.18 0,0 0,-0.1 0l-6.51,3.26L7,16.75a0.2,0.2 0,0 0,0.17 0.29,0.18 0.18,0 0,0 0.1,0l6.51,-3.26L17,7.25A0.2,0.2 0,0 0,16.85 7Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,1A11,11 0,1 0,23 12,11 11,0 0,0 12,1ZM17,7.25 L13.74,13.76L7.25,17A0.2,0.2 0,0 1,7 16.75l3.26,-6.51L16.75,7A0.2,0.2 0,0 1,17 7.25Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_bottom_person_e" android:state_checked="false" />
<item android:drawable="@drawable/ic_bottom_person_s" android:state_checked="true" />
</selector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2.5A4.5,4.5 0,1 1,7.5 7,4.51 4.51,0 0,1 12,2.5M12,1a6,6 0,1 0,6 6,6 6,0 0,0 -6,-6Z"
android:fillColor="#2f45a6" />
<path
android:pathData="M3.5,21A4.49,4.49 0,0 1,8 16.5h8A4.49,4.49 0,0 1,20.5 21v2H21a1,1 0,0 0,1 -1V21a6,6 0,0 0,-6 -6H8a6,6 0,0 0,-6 6v1a1,1 0,0 0,1 1h0.5Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,7m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#2f45a6" />
<path
android:pathData="M8,15h8a6,6 0,0 1,6 6v1a1,1 0,0 1,-1 1H3a1,1 0,0 1,-1 -1V21A6,6 0,0 1,8 15Z"
android:fillColor="#2f45a6" />
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_bottom_rss_feed_e" android:state_checked="false" />
<item android:drawable="@drawable/ic_bottom_rss_feed_s" android:state_checked="true" />
</selector>

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

Loading…
Cancel
Save