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. 12
      app/src/main/java/io/legado/app/data/entities/RssStar.kt
  9. 10
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  10. 40
      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. 43
      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. 18
      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. 17
      app/src/main/java/io/legado/app/model/WebBook.kt
  21. 6
      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. 2
      app/src/main/java/io/legado/app/service/DownloadService.kt
  25. 11
      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. 13
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  33. 5
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt
  34. 2
      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. 27
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  37. 4
      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. 54
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  41. 94
      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. 88
      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. 12
      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. 2
      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. 4
      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. 19
      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. 68
      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. 3
      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. 27
      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" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.4' def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime:2.1.1' implementation 'androidx.paging:paging-runtime:2.1.2'
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'

@ -23,6 +23,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true" android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme.Light" android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口--> <!--主入口-->
@ -78,6 +79,48 @@
android:resource="@xml/shortcuts" android:resource="@xml/shortcuts"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
</activity> </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 <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
@ -91,7 +134,7 @@
<!--书籍详情页--> <!--书籍详情页-->
<activity <activity
android:name=".ui.book.info.BookInfoActivity" android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTask" /> android:launchMode="singleTop" />
<!--书籍信息编辑--> <!--书籍信息编辑-->
<activity <activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" 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)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(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** **2020/03/11**
* 修复调节上边距时下边距一起动的bug * 修复调节上边距时下边距一起动的bug
* 适配沚水的web阅读 by 六月 * 适配沚水的web阅读 by 六月
* 分组管理页面调整 by yangyxd * 分组管理页面调整 by yangyxd
* 导入本地添加需要权限模式
**2020/03/10** **2020/03/10**
* 优化文字选择菜单弹出位置 * 优化文字选择菜单弹出位置

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

@ -50,9 +50,12 @@ interface BookSourceDao {
@get:Query("select * from book_sources order by customOrder asc") @get:Query("select * from book_sources order by customOrder asc")
val all: List<BookSource> 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> 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") @Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource? fun getBookSource(key: String): BookSource?

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

@ -16,4 +16,16 @@ data class RssStar(
var description: String? = null, var description: String? = null,
var content: String? = null, var content: String? = null,
var image: 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 android.os.Build
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.welcome.Launcher1 import io.legado.app.ui.welcome.*
import io.legado.app.ui.welcome.Launcher2
import io.legado.app.ui.welcome.Launcher3
import io.legado.app.ui.welcome.WelcomeActivity
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
/** /**
@ -20,7 +17,10 @@ object LauncherIconHelp {
private val componentNames = arrayListOf( private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name), ComponentName(App.INSTANCE, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::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?) { fun changeIcon(icon: String?) {

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

@ -1,8 +1,11 @@
package io.legado.app.help.permission package io.legado.app.help.permission
import android.os.Build
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.jetbrains.anko.startActivity
import java.util.ArrayList
class PermissionsCompat private constructor() { class PermissionsCompat private constructor() {
@ -12,6 +15,17 @@ class PermissionsCompat private constructor() {
RequestManager.pushRequest(request) 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 { class Builder {
private val request: Request private val request: Request

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

@ -21,14 +21,6 @@ object Backup {
FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath 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 { val backupFileNames by lazy {
arrayOf( arrayOf(
"bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json", "bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
@ -42,18 +34,16 @@ object Backup {
return return
} }
Coroutine.async { Coroutine.async {
val backupPath = context.getPrefString(PreferKey.backupPath) context.getPrefString(PreferKey.backupPath)?.let {
if (backupPath.isNullOrEmpty()) { backup(context, it, true)
backup(context)
} else {
backup(context, backupPath)
} }
} }
} }
suspend fun backup(context: Context, path: String = legadoPath) { suspend fun backup(context: Context, path: String, isAuto: Boolean = false) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
synchronized(this@Backup) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath) writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath) writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath) writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
@ -80,9 +70,10 @@ object Backup {
} }
WebDavHelp.backUpWebDav(backupPath) WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) { if (path.isContentPath()) {
copyBackup(context, Uri.parse(path)) copyBackup(context, Uri.parse(path), isAuto)
} else { } else {
copyBackup(File(path)) copyBackup(File(path), isAuto)
}
} }
} }
} }
@ -95,11 +86,19 @@ object Backup {
} }
@Throws(java.lang.Exception::class) @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 -> DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)
if (file.exists()) { if (file.exists()) {
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.findFile(fileName)?.delete()
treeDoc.createFile("", fileName) treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes()) ?.writeBytes(context, file.readBytes())
@ -107,15 +106,19 @@ object Backup {
} }
} }
} }
}
@Throws(java.lang.Exception::class) @Throws(java.lang.Exception::class)
private fun copyBackup(rootFile: File) { private fun copyBackup(rootFile: File, isAuto: Boolean) {
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)
if (file.exists()) { if (file.exists()) {
file.copyTo( file.copyTo(
FileUtils.createFileIfNotExist(rootFile, fileName), if (isAuto) {
true FileUtils.createFileIfNotExist(rootFile, fileName, "auto")
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
) )
} }
} }

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

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

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

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

@ -21,7 +21,10 @@ object Rss {
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> { ): Coroutine<Result> {
return Coroutine.async(scope, context) { 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 val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource) RssParserByRule.parseXML(body, rssSource)
} }
@ -30,12 +33,15 @@ object Rss {
fun getContent( fun getContent(
rssArticle: RssArticle, rssArticle: RssArticle,
ruleContent: String, ruleContent: String,
rssSource: RssSource?,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = AnalyzeUrl(rssArticle.link, baseUrl = rssArticle.origin) val body = AnalyzeUrl(
.getResponseAwait(rssArticle.origin) rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap()
).getResponseAwait(rssArticle.origin)
.body .body
val analyzeRule = AnalyzeRule() val analyzeRule = AnalyzeRule()
analyzeRule.setContent( 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,8 +27,17 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1, page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> { ): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
searchBookSuspend(scope, key, page)
}
}
suspend fun searchBookSuspend(
scope: CoroutineScope,
key: String,
page: Int? = 1
): ArrayList<SearchBook> {
bookSource.searchUrl?.let { searchUrl -> bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl, ruleUrl = searchUrl,
@ -38,15 +47,16 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList( return BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,
res.url, res.url,
true true
) )
} ?: arrayListOf()
} }
return arrayListOf()
} }
/** /**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList( BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,

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

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

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

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

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

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

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

@ -6,12 +6,14 @@ import android.view.MenuItem
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup 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.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
@ -64,6 +66,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
recycler_view.addItemDecoration(VerticalDivider(this)) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this) adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter 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.setMainActionText(R.string.move_to_group)
select_action_bar.inflateMenu(R.menu.arrange_book_sel) select_action_bar.inflateMenu(R.menu.arrange_book_sel)
select_action_bar.setOnMenuItemClickListener(this) 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.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.gone import io.legado.app.help.ItemTouchCallback
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_arrange_book.view.* import kotlinx.android.synthetic.main.item_arrange_book.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
class ArrangeBookAdapter(context: Context, val callBack: CallBack) : 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 val groupRequestCode = 12
private val selectedBooks: HashSet<Book> = hashSetOf() private val selectedBooks: HashSet<Book> = hashSetOf()
var actionItem: Book? = null 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>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
tv_name.text = item.name tv_name.text = item.name
// tv_name.text = if (item.author.isEmpty()) { tv_author.text = item.author
// item.name
// } else {
// "${item.name}(${item.author})"
// }
tv_author.text = item.author // resources.getString(R.string.author_show, item.author)
tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE
tv_group_s.text = getGroupName(item.group) tv_group_s.text = getGroupName(item.group)
checkbox.isChecked = selectedBooks.contains(item) checkbox.isChecked = selectedBooks.contains(item)
@ -115,7 +109,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
groupNames.add(it.groupName) groupNames.add(it.groupName)
} }
} }
return groupNames; return groupNames
} }
private fun getGroupName(groupId: Int): String { private fun getGroupName(groupId: Int): String {

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

@ -72,14 +72,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
fun search() { fun search() {
task = execute { task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { .onSuccess(IO) {
it?.forEach { searchBook -> it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) { if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) { if (searchBook.tocUrl.isEmpty()) {
@ -95,6 +94,10 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
} }
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {
@ -107,7 +110,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess { .onSuccess {
it?.let { loadChapter(it) } loadChapter(it)
}.onError { }.onError {
debug { context.getString(R.string.error_get_book_info) } debug { context.getString(R.string.error_get_book_info) }
} }
@ -119,14 +122,12 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute { execute {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) { chapters ->
it?.let { chapters ->
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook() val searchBook: SearchBook = book.toSearchBook()
searchFinish(searchBook) searchFinish(searchBook)
} }
}
}.onError { }.onError {
debug { context.getString(R.string.error_get_chapter_list) } 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 var durChapterIndex = 0
private lateinit var mLayoutManager: UpLinearLayoutManager private lateinit var mLayoutManager: UpLinearLayoutManager
private var tocLiveData: LiveData<List<BookChapter>>? = null private var tocLiveData: LiveData<List<BookChapter>>? = null
private var scrollToDurChapter = false
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.chapterCallBack = this viewModel.chapterCallBack = this
@ -74,7 +75,6 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.let { book?.let {
durChapterIndex = it.durChapterIndex durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle tv_current_chapter_info.text = it.durChapterTitle
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
initCacheFileNames(it) initCacheFileNames(it)
} }
} }
@ -85,7 +85,10 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl) tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl)
tocLiveData?.observe(viewLifecycleOwner, Observer { tocLiveData?.observe(viewLifecycleOwner, Observer {
adapter.setItems(it) adapter.setItems(it)
if (!scrollToDurChapter) {
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
scrollToDurChapter = true
}
}) })
} }

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

@ -33,6 +33,7 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod 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_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.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.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
tv_ok.setTextColor(requireContext().accentColor) tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() } tv_ok.onClick { dismiss() }
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult = val diffResult =

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

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

@ -8,6 +8,7 @@ import android.os.Bundle
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -17,6 +18,8 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.help.AppConfig 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.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
@ -40,7 +43,8 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private val subDocs = arrayListOf<DocumentFile>() private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null 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 override val viewModel: ImportBookViewModel
get() = getViewModel(ImportBookViewModel::class.java) get() = getViewModel(ImportBookViewModel::class.java)
@ -79,6 +83,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
} }
} }
}) })
} }
private fun initEvent() { private fun initEvent() {
@ -123,6 +128,24 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
subDocs.clear() subDocs.clear()
path = it 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() upPath()
} }
@ -159,7 +182,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
} }
} }
} ?: let { } ?: let {
tv_path.text = path tv_path.text = path.replace(sdPath, "SD")
val docList = arrayListOf<DocItem>() val docList = arrayListOf<DocItem>()
File(path).listFiles()?.forEach { File(path).listFiles()?.forEach {
if (it.isDirectory) { if (it.isDirectory) {
@ -228,7 +251,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
@Synchronized @Synchronized
private fun goBackDir(): Boolean { private fun goBackDir(): Boolean {
if (rootDoc == null) { if (rootDoc == null) {
if (path != FileUtils.getSdCardPath()) { if (path != sdPath) {
File(path).parent?.let { File(path).parent?.let {
path = it path = it
upPath() upPath()

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

@ -39,7 +39,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun initBook(book: Book) { private fun initBook(book: Book) {
if (ReadBook.book?.bookUrl != book.bookUrl) { if (ReadBook.book?.bookUrl != book.bookUrl) {
ReadBook.resetData(book) ReadBook.resetData(book) { name, author ->
autoChangeSource(name, author)
}
isInitFinish = true isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) { if (ReadBook.chapterSize == 0) {
@ -52,7 +54,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) { if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1 ReadBook.durChapterIndex = ReadBook.chapterSize - 1
} }
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
if (ReadBook.inBookshelf) { if (ReadBook.inBookshelf) {
ReadBook.saveRead() ReadBook.saveRead()
@ -60,7 +62,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else { } else {
isInitFinish = true isInitFinish = true
ReadBook.titleDate.postValue(book.name) 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) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) { if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) { if (book.tocUrl.isEmpty()) {
@ -70,7 +74,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} else { } else {
if (ReadBook.curTextChapter != null) { 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.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book) App.db.bookDao().update(book)
ReadBook.chapterSize = it.size ReadBook.chapterSize = it.size
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
} else { } else {
ReadBook.webBook?.getChapterList(book, this) ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList -> ?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book) App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size ReadBook.chapterSize = cList.size
ReadBook.loadContent() ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
} else { } else {
changeDruChapterIndex(cList) changeDruChapterIndex(cList)
} }
} else { } else {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} }
}?.onError { }?.onError {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} ?: autoChangeSource() }
} }
} }
} }
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
ReadBook.upMsg(null)
ReadBook.book?.let { ReadBook.book?.let {
App.db.bookDao().delete(it) 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>) { 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.bookDao().update(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray()) App.db.bookChapterDao().insert(*chapters.toTypedArray())
ReadBook.chapterSize = chapters.size ReadBook.chapterSize = chapters.size
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
} }
@ -181,7 +205,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durPageIndex = pageIndex ReadBook.durPageIndex = pageIndex
} }
ReadBook.saveRead() ReadBook.saveRead()
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
@ -209,7 +233,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex) App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex)
?.let { chapter -> ?.let { chapter ->
BookHelp.delContent(book, chapter) BookHelp.delContent(book, chapter)
ReadBook.loadContent(ReadBook.durChapterIndex) ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
} }
} }
} }

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.speech.tts.TextToSpeech
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
@ -15,13 +16,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.service.BaseReadAloudService
import io.legado.app.utils.gone
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.visible 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.sdk27.listeners.onClick
import org.jetbrains.anko.share import org.jetbrains.anko.share
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.util.*
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class TextActionMenu(private val context: Context, private val callBack: CallBack) : 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 { init {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -44,33 +50,38 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
isFocusable = false isFocusable = false
initRecyclerView() 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) { private fun initRecyclerView() = with(contentView) {
val adapter = Adapter(context)
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val menu = MenuBuilder(context) recycler_view_more.adapter = adapter
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems) adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val popupMenu = PopupMenu(context, iv_menu_more) onInitializeMenu(moreMenu)
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
} }
if (moreMenu.size() > 0) {
iv_menu_more.visible()
} }
iv_menu_more.onClick { iv_menu_more.onClick {
popupMenu.show() 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) { when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText) R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(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 -> { R.id.menu_browser -> {
try { try {
val intent = if (callBack.selectedText.isAbsUrl()) { 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") 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) @RequiresApi(Build.VERSION_CODES.M)
private fun createProcessTextIntent(): Intent { private fun createProcessTextIntent(): Intent {
return Intent() return Intent()

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

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

@ -38,7 +38,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else { } else {
ReadBook.setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
if (upContent) upContent() if (upContent) upContent(resetPageOffset = false)
true true
} else } else
false false
@ -51,7 +51,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else { } else {
ReadBook.setPageIndex(pageIndex.minus(1)) ReadBook.setPageIndex(pageIndex.minus(1))
} }
if (upContent) upContent() if (upContent) upContent(resetPageOffset = false)
true true
} else } else
false false
@ -59,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val currentPage: TextPage override val currentPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let { currentChapter?.let {
return@with it.page(pageIndex) return@with it.page(pageIndex)
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
@ -68,6 +71,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val nextPage: TextPage override val nextPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let { currentChapter?.let {
if (pageIndex < it.pageSize() - 1) { if (pageIndex < it.pageSize() - 1) {
return@with it.page(pageIndex + 1)?.removePageAloudSpan() return@with it.page(pageIndex + 1)?.removePageAloudSpan()
@ -86,6 +92,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val prevPage: TextPage override val prevPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
if (pageIndex > 0) { if (pageIndex > 0) {
currentChapter?.let { currentChapter?.let {
return@with it.page(pageIndex - 1)?.removePageAloudSpan() 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? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle() val payload = Bundle()
val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
payload.putString("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) { if (oldItem.intro != newItem.intro) {
payload.putString("intro", newItem.intro) payload.putString("intro", newItem.intro)
} }
if (payload.isEmpty) {
return null
}
return payload return payload
} }
} }

@ -112,6 +112,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
viewModel.saveSearchKey(query) viewModel.saveSearchKey(query)
viewModel.search(it) viewModel.search(it)
} }
openOrCloseHistory(false)
return true return true
} }
@ -128,6 +129,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
openOrCloseHistory(hasFocus) openOrCloseHistory(hasFocus)
} }
} }
openOrCloseHistory(true)
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -283,10 +285,12 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
*/ */
@Synchronized @Synchronized
private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) { private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) {
val searchItems = ArrayList(items)
if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) { if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
refreshTime = System.currentTimeMillis() refreshTime = System.currentTimeMillis()
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items)) val diffResult =
adapter.setItems(items, 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.constant.PreferKey
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig import io.legado.app.model.SearchBookModel
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.isActive
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application)
private var searchPool = , SearchBookModel.CallBack {
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() private val searchBookModel = SearchBookModel(this, this)
private var task: Coroutine<*>? = null
var isSearchLiveData = MutableLiveData<Boolean>() var isSearchLiveData = MutableLiveData<Boolean>()
var searchBookLiveData = MutableLiveData<List<SearchBook>>() var searchBookLiveData = MutableLiveData<List<SearchBook>>()
var searchKey: String = "" var searchKey: String = ""
var searchPage = 1
var isLoading = false var isLoading = false
var searchBooks = arrayListOf<SearchBook>() var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L
/** /**
* 开始搜索 * 开始搜索
*/ */
fun search(key: String) { fun search(key: String) {
task?.cancel() if (searchKey != key && key.isNotEmpty()) {
if (key.isEmpty() && searchKey.isEmpty()) { searchBookModel.cancelSearch()
return
} else if (key.isEmpty()) {
isLoading = true
searchPage++
} else if (key.isNotEmpty()) {
isLoading = true
searchPage = 1
searchKey = key
searchBooks.clear() searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
} }
searchBookModel.search(searchID, searchKey)
}
override fun onSearchStart() {
isSearchLiveData.postValue(true) isSearchLiveData.postValue(true)
task = execute { }
val searchGroup = context.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) { override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
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)) { if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(list) precisionSearch(this, searchBooks)
} else { } else {
App.db.searchBookDao().insert(*list.toTypedArray()) App.db.searchBookDao().insert(*searchBooks.toTypedArray())
mergeItems(list) mergeItems(this, searchBooks)
}
}
}
} }
} }
task?.invokeOnCompletion { override fun onSearchFinish() {
isSearchLiveData.postValue(false) isSearchLiveData.postValue(false)
isLoading = 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>() val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook -> searchBooks.forEach { searchBook ->
if (searchBook.name.contains(searchKey, true) if (searchBook.name.contains(searchKey, true)
@ -90,14 +70,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
) books.add(searchBook) ) books.add(searchBook)
} }
App.db.searchBookDao().insert(*books.toTypedArray()) App.db.searchBookDao().insert(*books.toTypedArray())
mergeItems(books) if (scope.isActive) {
mergeItems(scope, books)
}
} }
/** /**
* 合并搜索结果并排序 * 合并搜索结果并排序
*/ */
@Synchronized @Synchronized
private fun mergeItems(newDataS: List<SearchBook>) { private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>) {
if (newDataS.isNotEmpty()) { if (newDataS.isNotEmpty()) {
val copyDataS = ArrayList(searchBooks) val copyDataS = ArrayList(searchBooks)
val searchBooksAdd = ArrayList<SearchBook>() val searchBooksAdd = ArrayList<SearchBook>()
@ -141,6 +123,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
if (!scope.isActive) return
searchBooks.sortWith(Comparator { o1, o2 -> searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) { if (o1.name == searchKey && o2.name != searchKey) {
1 1
@ -166,6 +149,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
0 0
} }
}) })
if (!scope.isActive) return
searchBooks = copyDataS searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS) searchBookLiveData.postValue(copyDataS)
} }
@ -175,7 +159,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
* 停止搜索 * 停止搜索
*/ */
fun stop() { fun stop() {
task?.cancel() searchBookModel.cancelSearch()
} }
/** /**
@ -211,7 +195,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
override fun onCleared() { override fun onCleared() {
super.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.base.BaseViewModel
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.http.HttpHelper 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.OldRule
import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.* import io.legado.app.utils.*
@ -96,7 +95,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
FileUtils.createFileIfNotExist(file, "exportBookSource.json") FileUtils.createFileIfNotExist(file, "exportBookSource.json")
.writeText(json) .writeText(json)
}.onSuccess { }.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}") context.toast("成功导出至\n${file.absolutePath}")
}.onError { }.onError {
context.toast("导出失败\n${it.localizedMessage}") context.toast("导出失败\n${it.localizedMessage}")
} }
@ -109,7 +108,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
doc.createFile("", "exportBookSource.json") doc.createFile("", "exportBookSource.json")
?.writeText(context, json) ?.writeText(context, json)
}.onSuccess { }.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}") context.toast("成功导出至\n${doc.uri.path}")
}.onError { }.onError {
context.toast("导出失败\n${it.localizedMessage}") context.toast("导出失败\n${it.localizedMessage}")
} }
@ -211,7 +210,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}.onError { }.onError {
finally(it.localizedMessage ?: "") finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it ?: "导入完成") finally(it)
} }
} }

@ -48,23 +48,19 @@ object BackupRestoreUi {
selectBackupFolder(fragment, backupSelectRequestCode) selectBackupFolder(fragment, backupSelectRequestCode)
} }
} else { } else {
backupUsePermission(fragment, requestCode = backupSelectRequestCode) backupUsePermission(fragment, backupPath)
} }
} }
} }
private fun backupUsePermission( private fun backupUsePermission(
fragment: Fragment, fragment: Fragment,
path: String = Backup.legadoPath, path: String
requestCode: Int = selectFolderRequestCode
) { ) {
PermissionsCompat.Builder(fragment) PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE) .addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage) .rationale(R.string.tip_perm_request_storage)
.onGranted { .onGranted {
when (requestCode) {
selectFolderRequestCode -> AppConfig.backupPath = Backup.legadoPath
else -> {
Coroutine.async { Coroutine.async {
AppConfig.backupPath = path AppConfig.backupPath = path
Backup.backup(fragment.requireContext(), path) Backup.backup(fragment.requireContext(), path)
@ -72,8 +68,6 @@ object BackupRestoreUi {
fragment.toast(R.string.backup_success) fragment.toast(R.string.backup_success)
} }
} }
}
}
.request() .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) PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE) .addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage) .rationale(R.string.tip_perm_request_storage)

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

@ -15,7 +15,6 @@ import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud 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) putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
val log = String(assets.open("updateLog.md").readBytes()) 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) return super.onKeyUp(keyCode, event)
} }
override fun finish() {
if (!BuildConfig.DEBUG) {
Backup.autoBack(this)
}
super.finish()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
ReadAloud.stop(this) ReadAloud.stop(this)

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

@ -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.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.SearchView 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.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseFragment 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.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.* import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
@ -42,15 +45,14 @@ import org.jetbrains.anko.startActivity
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf), class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener, TabLayout.OnTabSelectedListener,
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
GroupManageDialog.CallBack, GroupManageDialog.CallBack {
BookshelfAdapter.CallBack {
override val viewModel: BookshelfViewModel override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java) get() = getViewModel(BookshelfViewModel::class.java)
private lateinit var bookshelfAdapter: BookshelfAdapter
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>() private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Int, Fragment>()
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
setSupportToolbar(toolbar) 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 private val selectedGroup: BookGroup
get() = bookGroups[view_pager_bookshelf.currentItem] get() = bookGroups[view_pager_bookshelf.currentItem]
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(view_pager_bookshelf)
tab_layout.isTabIndicatorFullWidth = false tab_layout.isTabIndicatorFullWidth = false
tab_layout.tabMode = TabLayout.MODE_SCROLLABLE tab_layout.tabMode = TabLayout.MODE_SCROLLABLE
tab_layout.setSelectedTabIndicatorColor(requireContext().accentColor) tab_layout.setSelectedTabIndicatorColor(requireContext().accentColor)
ATH.applyEdgeEffectColor(view_pager_bookshelf) tab_layout.setupWithViewPager(view_pager_bookshelf)
bookshelfAdapter = BookshelfAdapter(this, this) view_pager_bookshelf.offscreenPageLimit = 1
view_pager_bookshelf.adapter = bookshelfAdapter view_pager_bookshelf.adapter = TabFragmentPageAdapter(childFragmentManager)
TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position ->
tab.text = bookGroups[position].groupName
}.attach()
} }
private fun initBookGroupData() { private fun initBookGroupData() {
@ -119,7 +112,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
bookGroups.add(AppConst.bookGroupAudio) bookGroups.add(AppConst.bookGroupAudio)
} }
bookGroups.addAll(it) bookGroups.addAll(it)
bookshelfAdapter.notifyDataSetChanged() view_pager_bookshelf.adapter?.notifyDataSetChanged()
tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select()
tab_layout.addOnTabSelectedListener(this) tab_layout.addOnTabSelectedListener(this)
} }
@ -149,7 +142,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
if (AppConfig.bookGroupAllShow) { if (AppConfig.bookGroupAllShow) {
bookGroups.add(0, AppConst.bookGroupAll) 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) 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) WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let { book -> App.db.bookDao().insert(it)
App.db.bookDao().insert(book)
successCount++ successCount++
}
}.onError { }.onError {
throw Exception(it.localizedMessage) throw Exception(it.localizedMessage)
} }

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

@ -7,6 +7,7 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
@ -26,6 +27,7 @@ import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_books.* import kotlinx.android.synthetic.main.fragment_books.*
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import kotlin.math.max
class BooksFragment : BaseFragment(R.layout.fragment_books), class BooksFragment : BaseFragment(R.layout.fragment_books),
@ -74,6 +76,23 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
booksAdapter = BooksAdapterGrid(requireContext(), this) booksAdapter = BooksAdapterGrid(requireContext(), this)
} }
rv_bookshelf.adapter = booksAdapter 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() { 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.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import io.legado.app.utils.dp
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.* 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) : class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) :
SimpleRecyclerAdapter<BookSource>(context, R.layout.item_find_book) { SimpleRecyclerAdapter<BookSource>(context, R.layout.item_find_book) {
private var exIndex = 0 private var exIndex = -1
private var scrollTo = -1 private var scrollTo = -1
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) { 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()) { if (payloads.isEmpty()) {
tv_name.text = item.bookSourceName tv_name.text = item.bookSourceName
} }
if (exIndex == holder.layoutPosition) { 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.loadingColor = context.accentColor
rotate_loading.show() rotate_loading.show()
if (scrollTo >= 0) { if (scrollTo >= 0) {
@ -68,7 +74,7 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
} }
} }
} else { } else {
iv_status.setImageResource(R.drawable.ic_add) iv_status.setImageResource(R.drawable.ic_arrow_right)
rotate_loading.hide() rotate_loading.hide()
gl_child.gone() 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.config.ConfigViewModel
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.prefs.NameListPreference import io.legado.app.ui.widget.prefs.NameListPreference
import io.legado.app.ui.widget.prefs.PreferenceCategory import io.legado.app.ui.widget.prefs.PreferenceCategory
import io.legado.app.ui.widget.prefs.SwitchPreference 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) { override fun onCompatOptionsItemSelected(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.menu_help -> startActivity<AboutActivity>() R.id.menu_help -> {
R.id.menu_backup -> BackupRestoreUi.backup(this) val text = String(requireContext().assets.open("help.md").readBytes())
R.id.menu_restore -> BackupRestoreUi.restore(this) 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.base.BaseViewModel
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.http.HttpHelper 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.help.storage.ImportOldData
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -108,7 +107,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
FileUtils.createFileIfNotExist(file, "exportReplaceRule.json") FileUtils.createFileIfNotExist(file, "exportReplaceRule.json")
.writeText(json) .writeText(json)
}.onSuccess { }.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}") context.toast("成功导出至\n${file.absolutePath}")
}.onError { }.onError {
context.toast("导出失败\n${it.localizedMessage}") context.toast("导出失败\n${it.localizedMessage}")
} }
@ -121,7 +120,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
doc.createFile("", "exportReplaceRule.json") doc.createFile("", "exportReplaceRule.json")
?.writeText(context, json) ?.writeText(context, json)
}.onSuccess { }.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}") context.toast("成功导出至\n${doc.uri.path}")
}.onError { }.onError {
context.toast("导出失败\n${it.localizedMessage}") context.toast("导出失败\n${it.localizedMessage}")
} }

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

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

@ -15,6 +15,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource 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.help.http.HttpHelper
import io.legado.app.model.Rss import io.legado.app.model.Rss
import io.legado.app.model.analyzeRule.AnalyzeUrl 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.FileUtils
import io.legado.app.utils.isContentPath import io.legado.app.utils.isContentPath
import io.legado.app.utils.writeBytes import io.legado.app.utils.writeBytes
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -34,8 +36,10 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
var rssArticle: RssArticle? = null var rssArticle: RssArticle? = null
val contentLiveData = MutableLiveData<String>() val contentLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<AnalyzeUrl>() val urlLiveData = MutableLiveData<AnalyzeUrl>()
var star = false var rssStar: RssStar? = null
var textToSpeech: TextToSpeech = TextToSpeech(context, this) var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
private var ttsTextList = arrayListOf<String>()
fun initData(intent: Intent) { fun initData(intent: Intent) {
execute { execute {
@ -43,8 +47,8 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
val link = intent.getStringExtra("link") val link = intent.getStringExtra("link")
if (origin != null && link != null) { if (origin != null && link != null) {
rssSource = App.db.rssSourceDao().getByKey(origin) rssSource = App.db.rssSourceDao().getByKey(origin)
star = App.db.rssStarDao().get(origin, link) != null rssStar = App.db.rssStarDao().get(origin, link)
rssArticle = App.db.rssArticleDao().get(origin, link) rssArticle = rssStar?.toRssArticle() ?: App.db.rssArticleDao().get(origin, link)
rssArticle?.let { rssArticle -> rssArticle?.let { rssArticle ->
if (!rssArticle.description.isNullOrBlank()) { if (!rssArticle.description.isNullOrBlank()) {
contentLiveData.postValue(rssArticle.description) contentLiveData.postValue(rssArticle.description)
@ -76,21 +80,26 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
} }
private fun loadContent(rssArticle: RssArticle, ruleContent: String) { private fun loadContent(rssArticle: RssArticle, ruleContent: String) {
Rss.getContent(rssArticle, ruleContent, this) Rss.getContent(rssArticle, ruleContent, rssSource, this)
.onSuccess { .onSuccess(IO) { body ->
contentLiveData.postValue(it) rssArticle.description = body
App.db.rssArticleDao().insert(rssArticle)
rssStar?.let {
it.description = body
App.db.rssStarDao().insert(it)
}
contentLiveData.postValue(body)
} }
} }
fun favorite() { fun favorite() {
execute { execute {
rssArticle?.let { rssStar?.let {
if (star) {
App.db.rssStarDao().delete(it.origin, it.link) App.db.rssStarDao().delete(it.origin, it.link)
} else { rssStar = null
App.db.rssStarDao().insert(it.toStar()) } ?: rssArticle?.toStar()?.let {
} App.db.rssStarDao().insert(it)
star = !star rssStar = it
} }
}.onSuccess { }.onSuccess {
callBack?.upStarMenu() callBack?.upStarMenu()
@ -143,28 +152,43 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
} }
} }
@Synchronized
override fun onInit(status: Int) { override fun onInit(status: Int) {
launch {
if (status == TextToSpeech.SUCCESS) { if (status == TextToSpeech.SUCCESS) {
textToSpeech.language = Locale.CHINA textToSpeech?.language = Locale.CHINA
textToSpeech.setOnUtteranceProgressListener(TTSUtteranceListener()) textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsInitFinish = true
play()
} else { } else {
launch {
toast(R.string.tts_init_failed) toast(R.string.tts_init_failed)
} }
} }
} }
fun readAloud(text: String) { @Synchronized
textToSpeech.stop() private fun play() {
text.split("\n", " ", "  ").forEach { if (!ttsInitFinish) return
textToSpeech.speak(it, TextToSpeech.QUEUE_ADD, null, "rss") 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() { override fun onCleared() {
super.onCleared() super.onCleared()
textToSpeech.stop() textToSpeech?.stop()
textToSpeech.shutdown() 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.dialogs.yesButton
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.*
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.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.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
tv_ok.setTextColor(requireContext().accentColor) tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() } tv_ok.onClick { dismiss() }
App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()

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

@ -59,3 +59,6 @@ open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) {
class Launcher1 : WelcomeActivity() class Launcher1 : WelcomeActivity()
class Launcher2 : 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.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
@ -186,6 +187,11 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a
toolbar.setSubtitleTextAppearance(context, resId) toolbar.setSubtitleTextAppearance(context, resId)
} }
fun transparent() {
elevation = 0f
backgroundColor = Color.TRANSPARENT
}
private fun attachToActivity() { private fun attachToActivity() {
if (attachToActivity) { if (attachToActivity) {
activity?.let { activity?.let {

@ -5,29 +5,40 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import kotlinx.android.synthetic.main.dialog_text_view.* import kotlinx.android.synthetic.main.dialog_text_view.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
class TextDialog : DialogFragment() { class TextDialog : BaseDialogFragment() {
companion object { companion object {
const val MD = 1 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 { TextDialog().apply {
val bundle = Bundle() val bundle = Bundle()
bundle.putString("content", content) bundle.putString("content", content)
bundle.putInt("mode", mode) bundle.putInt("mode", mode)
bundle.putLong("time", time)
arguments = bundle arguments = bundle
isCancelable = false
}.show(fragmentManager, "textDialog") }.show(fragmentManager, "textDialog")
} }
} }
private var time = 0L
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = DisplayMetrics() val dm = DisplayMetrics()
@ -43,8 +54,7 @@ class TextDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_text_view, container) return inflater.inflate(R.layout.dialog_text_view, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let { arguments?.let {
val content = it.getString("content") ?: "" val content = it.getString("content") ?: ""
when (it.getInt("mode")) { when (it.getInt("mode")) {
@ -57,8 +67,27 @@ class TextDialog : DialogFragment() {
} }
else -> text_view.text = content 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 { init {
isPersistent = true isPersistent = true
layoutResource = io.legado.app.R.layout.view_preference
val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference) val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference)
showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true) 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) { 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) 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?) { 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?>() private val mEntryDrawables = arrayListOf<Drawable?>()
init { init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.view_icon widgetLayoutResource = R.layout.view_icon
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0) 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?) { override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder) 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) val selectedIndex = findIndexOfValue(value)
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
val drawable = mEntryDrawables[selectedIndex] 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) { class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) {
init { init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.item_fillet_text widgetLayoutResource = R.layout.item_fillet_text
} }
override fun onBindViewHolder(holder: PreferenceViewHolder?) { 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) 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 package io.legado.app.ui.widget.prefs
import android.content.Context import android.content.Context
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import io.legado.app.R
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
class PreferenceCategory(context: Context, attrs: AttributeSet) : class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) {
PreferenceCategory(context, attrs) {
init {
isPersistent = true
layoutResource = R.layout.view_preference_category
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) { override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
holder?.let { holder?.let {
val view = it.findViewById(android.R.id.title) val view = it.findViewById(R.id.preference_title)
if (view is TextView && !view.isInEditMode) { if (view is TextView) { // && !view.isInEditMode
view.text = title
view.setTextColor(context.accentColor) //设置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) : class SwitchPreference(context: Context, attrs: AttributeSet) :
SwitchPreferenceCompat(context, attrs) { SwitchPreferenceCompat(context, attrs) {
override fun onBindViewHolder(holder: PreferenceViewHolder?) { init {
super.onBindViewHolder(holder) layoutResource = R.layout.view_preference
holder?.let {
val view = it.findViewById(R.id.switchWidget)
if (view is SwitchCompat) {
ATH.setTint(view, context.accentColor)
} }
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
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_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_MIME_TYPE DocumentsContract.Document.COLUMN_MIME_TYPE
), null, null, null ), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME
) )
c?.let { c?.let {
val ici = c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) 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.os.Build
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.RadioGroup import android.widget.RadioGroup
import android.widget.SeekBar import android.widget.SeekBar
@ -75,6 +76,13 @@ fun View.screenshot(): Bitmap? {
}.getOrNull() }.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) { fun SeekBar.progressAdd(int: Int) {
progress += 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