Răsfoiți Sursa

Merge branch 'master' into master

Xwite 4 ani în urmă
părinte
comite
7007f1c04c
92 a modificat fișierele cu 1240 adăugiri și 1153 ștergeri
  1. BIN
      app/cronetlib/cronet_api.jar
  2. BIN
      app/cronetlib/cronet_impl_common_java.jar
  3. 4 0
      app/src/main/AndroidManifest.xml
  4. 1 1
      app/src/main/assets/cronet.json
  5. 14 1
      app/src/main/assets/help/ruleHelp.md
  6. 7 650
      app/src/main/assets/updateLog.md
  7. 0 0
      app/src/main/assets/web/bookshelf/css/detail.01ad55b1.css
  8. 2 2
      app/src/main/assets/web/bookshelf/index.html
  9. 0 0
      app/src/main/assets/web/bookshelf/js/about.a288ed22.js
  10. 0 0
      app/src/main/assets/web/bookshelf/js/app.cfea0936.js
  11. 0 0
      app/src/main/assets/web/bookshelf/js/detail.562e75a6.js
  12. 0 0
      app/src/main/assets/web/bookshelf/js/detail.b1145ea3.js
  13. 2 0
      app/src/main/java/io/legado/app/App.kt
  14. 18 24
      app/src/main/java/io/legado/app/api/controller/BookSourceController.kt
  15. 21 28
      app/src/main/java/io/legado/app/api/controller/RssSourceController.kt
  16. 1 1
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  17. 21 7
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  18. 5 1
      app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
  19. 20 25
      app/src/main/java/io/legado/app/data/entities/Book.kt
  20. 7 2
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  21. 13 11
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  22. 6 10
      app/src/main/java/io/legado/app/help/BookHelp.kt
  23. 5 7
      app/src/main/java/io/legado/app/help/DefaultData.kt
  24. 1 1
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  25. 48 13
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  26. 6 0
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  27. 4 3
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  28. 4 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  29. 2 2
      app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt
  30. 3 0
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  31. 1 0
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  32. 2 2
      app/src/main/java/io/legado/app/model/webBook/SearchModel.kt
  33. 23 20
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  34. 8 7
      app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
  35. 1 1
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  36. 4 24
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt
  37. 5 5
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  38. 4 5
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  39. 20 0
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  40. 29 0
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt
  41. 45 0
      app/src/main/java/io/legado/app/ui/book/arrange/ComposeView.kt
  42. 121 0
      app/src/main/java/io/legado/app/ui/book/arrange/SourcePickerDialog.kt
  43. 21 2
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt
  44. 8 50
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt
  45. 1 1
      app/src/main/java/io/legado/app/ui/book/audio/ComposeView.kt
  46. 47 0
      app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt
  47. 25 0
      app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt
  48. 60 0
      app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt
  49. 110 0
      app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt
  50. 4 3
      app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt
  51. 3 1
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt
  52. 40 21
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt
  53. 79 12
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
  54. 17 30
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt
  55. 0 29
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt
  56. 2 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  57. 10 58
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  58. 16 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  59. 19 30
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  60. 7 6
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  61. 3 5
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  62. 2 2
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  63. 3 0
      app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt
  64. 1 0
      app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt
  65. 4 1
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  66. 1 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  67. 2 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt
  68. 2 0
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  69. 2 2
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  70. 23 10
      app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
  71. 5 0
      app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt
  72. 16 0
      app/src/main/java/io/legado/app/utils/GsonExtensions.kt
  73. 8 0
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  74. 22 0
      app/src/main/res/layout/activity_all_bookmark.xml
  75. 14 5
      app/src/main/res/layout/activity_arrange_book.xml
  76. 1 0
      app/src/main/res/layout/activity_main.xml
  77. 100 0
      app/src/main/res/layout/dialog_source_picker.xml
  78. 1 0
      app/src/main/res/layout/dialog_text_view.xml
  79. 20 0
      app/src/main/res/layout/item_1line_text.xml
  80. 1 0
      app/src/main/res/layout/item_1line_text_and_del.xml
  81. 1 0
      app/src/main/res/layout/view_tab_layout.xml
  82. 1 0
      app/src/main/res/layout/view_tab_layout_min.xml
  83. 4 0
      app/src/main/res/menu/arrange_book_sel.xml
  84. 6 2
      app/src/main/res/values-es-rES/strings.xml
  85. 7 3
      app/src/main/res/values-ja-rJP/strings.xml
  86. 7 3
      app/src/main/res/values-pt-rBR/strings.xml
  87. 7 3
      app/src/main/res/values-zh-rHK/strings.xml
  88. 7 3
      app/src/main/res/values-zh-rTW/strings.xml
  89. 7 3
      app/src/main/res/values-zh/strings.xml
  90. 7 3
      app/src/main/res/values/strings.xml
  91. 7 0
      app/src/main/res/xml/pref_main.xml
  92. 1 1
      gradle.properties

BIN
app/cronetlib/cronet_api.jar


BIN
app/cronetlib/cronet_impl_common_java.jar


+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -293,6 +293,10 @@
         <activity
             android:name=".ui.rss.favorites.RssFavoritesActivity"
             android:launchMode="singleTop" />
+        <!-- 书签 -->
+        <activity
+            android:name=".ui.book.bookmark.AllBookmarkActivity"
+            android:launchMode="singleTop" />
         <!-- 缓存界面 -->
         <activity
             android:name=".ui.book.cache.CacheActivity"

+ 1 - 1
app/src/main/assets/cronet.json

@@ -1 +1 @@
-{"x86":"ff4b324fca11309c5e93a45f65970540","armeabi-v7a":"65e8bb0095d5be314ada2f523718f477","x86_64":"3dac75809f86b8edd1399a642bd26f59","arm64-v8a":"ac515c2995b4fb831419313056b1d3f0","version":"100.0.4896.58"}
+{"x86_64":"d08640c5057600d40e6582f292d858e2","arm64-v8a":"f99bb5a1e465754635e19b57204af9f5","armeabi-v7a":"f195422fc23bf985b120c9918091e9b8","x86":"ca9a32dc413021834c2b6a658656c5aa","version":"100.0.4896.79"}

+ 14 - 1
app/src/main/assets/help/ruleHelp.md

@@ -38,7 +38,20 @@ source.getLoginInfoMap().get("telephone")
     "telephone":"123456",
     "password":"123456"
 }
-
+source登录相关方法,可在js内通过source.调用,可以参考阿里云语音登录
+login()
+getHeaderMap(hasLoginHeader: Boolean = false)
+getLoginHeader(): String?
+getLoginHeaderMap(): Map<String, String>?
+putLoginHeader(header: String)
+removeLoginHeader()
+setVariable(variable: String?)
+getVariable(): String?
+AnalyzeUrl相关函数,js中通过java.调用
+initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问
+getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头
+getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果
+getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测
 ```
 
 * 发现url格式

+ 7 - 650
app/src/main/assets/updateLog.md

@@ -11,6 +11,12 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
+**2022/04/05**
+
+* 更新cronet: 100.0.4896.79
+* 添加所有书签
+* 添加批量换源
+
 **2022/03/31**
 
 * 添加java.importScript,详情见js帮助文档
@@ -205,653 +211,4 @@ eval(String(java.cacheFile(url)))
 **2022/01/01**
 
 * 修复本地txt问题,不在拷贝到私有目录,可以正常打开
-* 优化txt目录识别,取目录数量最多的规则
-
-**2021/12/28**
-
-* 用阅读打开本地朗读引擎文件和主体配置文件也可以导入
-* 缓存图片采用多线程
-* 本地书籍不在拷贝到私有目录,有权限的会直接打开,没权限的自己选择文件夹保存
-
-**2021/12/19**
-
-* 修复全面屏手势会触发翻页的bug
-* js添加java.logType(*),打印变量类型,方便调试时查看
-* 修复从发现中打开书籍是会打开其它书源书籍的bug
-* 全文搜索增加跳转上一个下一个的功能 by Jason Yao
-* post可以正确识别contentType
-
-**2021/12/10**
-
-* 朗读出错不弹出朗读界面的时候可以长按朗读按钮进入朗读界面切换朗读引擎,这个有很多人不知道
-* 修复cronet访问出错时应用崩溃的bug
-* 修复一些epub目录不全或内容不全的问题
-* 修复横屏双页时文字选择的问题
-* 电脑硬盘坏了还好资料恢复出来了,还是要经常备份比较好
-
-**2021/11/27**
-
-* 更新到SDK31,android 12
-* 修复在线朗读引擎新建会替换之前已有的bug
-* 修复目录界面自动跳转到顶部bug
-* 修复阿里云在线朗读引擎模板中获取时间的兼容性问题
-
-**2021/11/20**
-
-* 修复部分平板双页问题
-* 修复书源太多不能导出和备份的问题
-* 给txt目录规则添加了一个入口,在书源管理的菜单里面
-
-**2021/11/13**
-
-* 修复没有目录时进入阅读界面不自动更新目录的bug
-* 使用系统文件夹选择器出错时自动打开应用文件夹选择器,部分系统文件夹选择器被阉割了
-
-**2021/11/02**
-
-* 修复朗读错误时提示不消失的bug
-* 修复滚动阅读选择文字错位bug by DuShuYuan
-* 朗读语速调节添加微调按钮
-
-**2021/10/24**
-
-* 修复夜间模式不随系统变化的bug
-
-**2021/10/22**
-
-* 修复封面
-* 添加全局字体大小设置
-* 导入源和规则时可以先编辑再导入
-
-**2021/10/21**
-
-* 修复自定义封面会因为图片太大崩溃
-* 修复play版本一个会引起崩溃的bug
-
-**2021/10/17**
-
-* 修复朗读时可能会崩溃的bug
-
-**2021/10/16**
-
-* 再次修复朗读卡住问题
-* 导入书单改为多线程
-* 修复其它一些bug
-
-**2021/10/14**
-
-* 修复遇到一些存标点段朗读出错后不继续的问题
-* 朗读出错记录错误日志,现在很多界面的菜单里都可以打开日志
-
-**2021/10/10**
-
-* 阿里云语音自动登录
-* 修复一些bug
-* 优化阿里云登录,需重新登录
-
-```
-source登录相关方法,可在js内通过source.调用,可以参考阿里云语音登录
-login()
-getHeaderMap(hasLoginHeader: Boolean = false)
-getLoginHeader(): String?
-getLoginHeaderMap(): Map<String, String>?
-putLoginHeader(header: String)
-removeLoginHeader()
-setVariable(variable: String?)
-getVariable(): String?
-AnalyzeUrl相关函数,js中通过java.调用
-initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问
-getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头
-getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果
-getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测
-```
-
-**2021/10/07**
-
-1. 修复阅读界面长按菜单阻挡选择bug
-2. 添加订阅源api
-
-**2021/10/05**
-
-1. 优化阅读界面导航栏
-2. 规则添加代码高亮
-3. web写源添加订阅源
-4. httpTts朗读添加登录功能
-
-```
-返回语音之前加入了检测是否登录传入result为okhttp的Response,里面有headers和body,检测是否登录的js需返回正确的Response
-```
-
-**2021/10/02**
-
-1. 紧急修复弹出框崩溃bug
-2. 修复字体变粗后不能变回的bug
-3. 修复底部对齐有时无效的bug
-
-* 不要嫌更新得频繁,这是因为最近新加的功能比较多,出bug很正常,而且我是一个人写软件,没有测试人员,只有发出来大家一起找bug了,遇到bug及时反馈,能修复的我都会在第一时间修复
-
-**2021/10/01**
-
-1. 默认封面名称显示全
-2. 发现js错误时可以查看错误详情
-4. 修复rss标题显示url的问题
-5. 长按正文网址可以选择是否外部浏览器打开,会记住选择
-6. 添加书源操作按钮,编辑书源移到里面,增加网址点击区域包括书名,更容易点击
-7. 优化内置浏览器
-8. 其它一些优化
-
-**2021/09/29**
-
-1. 修复阅读界面导航栏挡住内容的bug
-2. 修复webView=ture是自动跳转移动网站的bug
-3. 导出添加进度条
-
-**2021/09/28**
-
-1. 添加横屏双页模式
-
-**2021/09/27**
-
-1. 非Play版本内置更新检测和下载,目前从github检测并下载, 不会自动提醒需手动检测, 可以关注公众号,比较重要的更新会在公众号发布然后可以在软件内更新
-2. js添加java.webView(html: String?, url: String?, js: String?): String?
-3. 修复一些bug
-
-**2021/09/22**
-
-1. 修复在线朗读遇到单独......崩溃的问题
-2. 有人提到在线朗读能及时翻页了,本地行不行,这个是要靠本地的tts支持的,我目前用的谷歌文字转语音就是支持的,其它的我不太清楚
-
-**2021/09/21**
-
-1. 阅读界面区域设置添加朗读上一段和朗读下一段
-2. 在线朗读采用平均速度计算及时翻页
-3. 修复听书定时问题
-4. 阅读小标题也使用替换
-
-**2021/09/20**
-
-1. 修复在线朗读跳段的bug
-2. 优化默认封面,添加显示书名作者的配置, 后面会添加书名和作者大小位置配置
-
-**2021/09/18**
-
-1. 朗读可以选择非默认tts
-2. 其它一些优化和bug修复
-
-**2021/09/16**
-
-1. 优化正文重复标题的去除,必须包含标题且标题后面有空格或换行才会去除,防止误删
-
-```^(\s|\p{P}|${name})*${title}(\s|\p{P})+```
-
-**2021/09/15**
-
-1. 修复因标题加入替换和简繁转换导致的缩进问题
-2. 如果出现标题不显示是净化规则的问题,可以先关闭净化
-
-**2021/09/14**
-
-1. 书架菜单添加了日志,更新失败的和下载失败的信息会显示在里面
-2. 书源添加校验关键字,有校验关键字的书源用此关键字校验
-3. 目录添加购买标识规则
-4. 正文标题使用替换,简繁转换,正文中书名,标题开头的自动去重
-5. 其它一些优化
-
-**2021/09/08**
-
-1. 优化离线缓存
-2. 听书界面添加登录菜单,和拷贝播放url
-3. 详情页添加设置源变量和书籍变量
-4. 订阅添加登录菜单,添加设置源变量
-
-**2021/09/06**
-
-1. 采用exoPlayer播放音频,支持更多格式
-2. 替换不再阻塞
-3. 修复详情页初始化:规则bug
-4. 书源内的并发率生效,两种格式
-    * 时间 格式: 如 500, 访问间隔500毫秒
-    * 次数/时间 格式: 如 5/60000, 每分钟最多访问5次
-
-5. url参数添加js, 和webJs参数,js参数传入url返回新的url, webJs参数在webView内执行直到返回不为空,和正文规则webJs一样
-6. 重写离线缓存
-
-**2021/09/01**
-
-1. 可以直接导出为链接,方便分享
-2. 修复一些bug
-
-**2021/08/28**
-
-1. 发现界面添加登录菜单
-2. 优化调试界面,预设搜索项可以点击
-3. 修复再没有webDav恢复时恢复按钮没反应的bug
-4. 自动阅读的设置改为选择翻页动画
-
-**2021/08/27**
-
-1. 修复导入书源问题
-2. 合并cornet版本,添加cornet开关
-3. 详情也选择分组后自动加入书架
-4. 书源管理可以筛选有登录url的书源,分组需登录
-5. 修复定时加快的问题
-7. 修复其它一些小bug
-
-**2021/08/24**
-
-1. 修复bug
-2. 可以加载证书过期网站的图片
-3. 修复书源不兼容老版本的问题
-4. 书源添加登录ui,和登录检测配置,稍后会给出示例,可以用来制作一些采用token登录的源,稍后会给出示例
-5. 修复web阅读进度没有同步到webDav的问题
-
-**2021/08/21**
-
-1. 阅读时自动更新最新章节
-2. 朗读添加媒体按键配置
-3. 修复rss列表界面分类往回切换时没有数据的bug
-4. 修复订阅分类往回切换时不显示内容的bug
-5. 导入书源防止非json格式导入
-6. 校验书源显示详细信息 by h11128
-7. 其它一些优化
-
-**2021/08/13**
-
-1. web传书可以使用
-2. 修复一些bug
-
-**2021/08/09**
-
-1. 修复选择文字不能选择单个文字的bug
-2. 分组可选择封面
-
-**2021/08/08**
-
-1. 背景图片添加模糊设置
-2. 书籍信息界面添加置顶操作
-3. 自动翻页时屏幕常亮
-4. 字典:中文使用百度汉语字典,英文使用海词字典。 by ag2s20150909
-5. 导入规则时可以选择添加分组还是替换分组
-
-**2021/08/02**
-
-1. 换源界面功能添加:置顶,置底,删除 by h11128
-2. Cronet:优化 by ag2s20150909
-3. 优化自动翻页 by jiuZhouWorlds
-4. 封面设置移到主题里面,白天和夜间可分别设置
-
-**2021/08/01**
-
-1. 为webService添加快捷操作
-2. 规则内替换使用正则错误时自动切换为不使用正则
-3. 优化Cronet
-4. 阅读界面菜单显示的时候停止按键翻页和自动阅读
-5. 切换后台停止自动阅读
-
-**2021/07/29**
-
-1. 修复每次更新都重新导入text规则的bug
-2. RSS阅读页添加刷新按钮以应对页面内容过期失效的BUG by JiuZhouWorlds
-3. 规则内替换使用正则报错时自动使用非正则替换
-
-**2021/07/27**
-
-1. 修复bug
-2. web使用api获取封面,不会再出现没有封面的情况
-3. 阅读亮度手动调节分别记住白天和夜间模式
-4. legado://import/auto?src={url}, 自动识别导入类型
-5. 一些优化并更新了一下web首页,感谢沚水, 传书暂时还不好用
-
-**2021/07/22**
-
-1. 非关键规则添加try防止报错中断解析
-2. 添加获取封面的api
-3. 获取正文api使用替换规则
-4. 添加一个ronet版本,网络访问使用Chromium内核
-5. web书架增加【最近一次更新书籍信息的时间】
-6. 采用Flow替换LiveData,优化资源使用
-7. 统一网络一键导入路径legado://import/{path}?src={url}
-
-* path: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig
-* 添加了txt小说规则,在线朗读引擎,主题,排版 的一键导入支持,老url依然可用
-
-8. 替换规则管理添加置顶所选和置底所选
-
-**2021/07/16**
-
-1. js扩展函数添加删除本地文件方法
-2. js扩展函数对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件,/android/data/{package}/cache/...
-
-**2021/07/15**
-
-1. 添加js函数来修复开启js沙箱后某些书源失效。by ag2s20150909
-
-```kotlin
-/**
- * 获取网络zip文件里面的数据
- * @param url zip文件的链接
- * @param path 所需获取文件在zip内的路径
- * @return zip指定文件的数据
- */
-fun getZipStringContent(url: String, path: String): String
-
-/**
- * 获取网络zip文件里面的数据
- * @param url zip文件的链接
- * @param path 所需获取文件在zip内的路径
- * @return zip指定文件的数据
- */
-fun getZipByteArrayContent(url: String, path: String): ByteArray?
-```
-
-* web服务添加一个导航页
-
-**2021/07/11**
-
-1. 开启JS沙箱限制
-
-* 禁止在js里exec运行命令
-* 禁止在js里通过geClass反射
-* 禁止在js里创建File对象
-* 禁止在js里获取Packages scope
-
-2. 优化并修复bug
-
-**2021/07/10**
-
-1. 阅读界面长按菜单改回原来样式
-2. 解决导入书源时重命名分组和保留名称冲突的问题
-
-**2021/07/09**
-
-1. 发现url添加json格式, 支持设置标签样式
-
-* 样式属性可以搜索 [FleboxLayout子元素支持的属性介绍](https://www.jianshu.com/p/3c471953e36d)
-* 样式属性可省略,有默认值
-
-**2021/07/07**
-
-1. 默认规则新增类似`jsonPath`的索引写法 by bushixuanqi
-
-* 格式形如 `[index,index, ...]` 或 `[!index,index, ...]` 其中`[!`开头表示筛选方式为排除,`index`可以是单个索引,也可以是区间。
-* 区间格式为 `start:end` 或 `start:end:step`,其中`start`为`0`可省略,`end`为`-1`可省略。
-* 索引、区间两端、区间间隔都支持负数
-* 例如 `tag.div[-1, 3:-2:-10, 2]`
-* 特殊用法 `tag.div[-1:0]` 可在任意地方让列表反向
-
-2. 允许索引作为@分段后每个部分的首规则,此时相当于前面是`children`
-
-* `head@.1@text` 与 `head@[1]@text` 与 `head@children[1]@text` 等价
-
-3. 添加Umd格式支持 by ag2s20150909
-4. 修复web页面按键重复监听的bug
-5. 亮度条往中间移了一点,防止误触
-6. 添加内置字典
-
-**2021/06/29**
-
-* 修复html格式化bug
-* 订阅界面webView支持css prefers-color-scheme: dark 查询,需webView v76或更高版本
-* 如webView低于v76可以用js调用activity.isNightTheme()来获取当前是否暗模式
-* 修复一些书籍导出epub失败 by ag2s20150909
-
-**2021/06/22**
-
-* 修复隐藏未读设置不生效的bug
-* 修复系统字体大小选择大时导入界面按钮显示不全的bug
-* 修复听书从后台打开时不对的bug
-
-**2021/06/20**
-
-* viewPager2 改回 viewPager
-* 添加配置导入文件规则功能 by bushixuanqi
-* 文件夹分组样式优化(未完成)
-* epub支持外部模板
-* 修复一些bug
-
-**2021/06/06**
-
-* 添加自定义导出文件名
-* 添加书架文件夹分组样式,未完成
-* viewPager2 3层嵌套有问题,书架换回viewPager
-
-**2021/05/29**
-
-* 谷歌版可使用外部epub模板
-* Asset文件夹下二级以内目录全文件读取,Asset->文件夹->文件
-* epub元数据修改,使修改字体只对正文生效
-* 修复epub模板文件的排序问题
-* epub可自定义模板,模板路径为书籍导出目录的Asset文件夹,[模板范例](https://wwa.lanzoux.com/ibjBspkn05i)
-
-```
-Asset中里面必须有Text文件夹,Text文件夹里必须有chapter.html,否则导出正文会为空
-chapter.html的关键字有{title}、{content}
-其他html文件的关键字有{name}、{author}、{intro}、{kind}、{wordCount}
-```
-
-**2021/05/24**
-
-* 反转目录后刷新内容
-* 修复上下滑动会导致左右切换问题
-* 精确搜索增加包含关键词的,比如搜索五行 五行天也显示出来, 五天行不显示
-
-**2021/05/21**
-
-* 添加反转目录功能
-* 修复分享bug
-* 详情页添加登录菜单
-* 添加发现界面隐藏配置
-
-**2021/05/16**
-
-* 添加总是使用默认封面配置
-* 添加一种语言 ptbr translation by mezysinc
-* epublib 修bug by ag2s20150909
-
-**2021/05/08**
-
-* 预下载章节可调整数目
-* 修复低版本Android使用TTS闪退。 by ag2s20150909
-* 修复WebDav报错
-* 优化翻页动画点击翻页
-
-**2021/05/06**
-
-* 修复bug
-* url参数添加重置次数,retry
-* 修改默认tts, 手动导入
-* 升级android studio
-
-**2021/04/30**
-
-* epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909
-* 添加高刷设置
-* 其它一些优化
-* pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本
-
-**2021/04/16**
-
-* 去掉google统计,解决华为手机使用崩溃的bug
-* 添加规则订阅时判断重复提醒
-* 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局
-
-**2021/04/08**
-
-* 缓存时重新检查并缓存图片
-* 订阅源调试添加源码查看
-* web调试不输出源码
-* 修复bug
-* 换源优化 --- by ag2s20150909
-* 修复localBook获取书名作者名的逻辑
-* 修复导出的epub的标题文字过大的bug
-* 优化图片排版
-
-**2021/04/02**
-
-* 修复bug
-* 书源调试添加源码查看
-* 添加导出epub by ag2s20150909
-* 换源添加是否校验作者选项
-
-**2021/03/31**
-
-* 优化epubLib by ag2s20150909
-* 升级库,修改弃用方法
-* tts引擎添加导入导出功能
-
-**2021/03/23**
-
-* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右
-* js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?>
-* 优化目录并发访问
-* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909
-
-**2021/03/19**
-
-* 修复图片地址参数缺少的bug
-* 修复更改替换规则时多次重新加载正文导致朗读多次停顿的bug
-* 修复是否使用替换默认值修改后不及时生效的bug
-* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 by hoodie13
-* 百度tsn改为tts
-
-**2021/03/15**
-
-* 优化图片TEXT样式显示
-* 图片url在解析正文时就拼接成绝对url
-* 修复一些bug
-
-**2021/03/08**
-
-* 阅读页面停留10分钟之后自动备份进度
-* 添加了针对中文的断行排版处理-by hoodie13, 需要再阅读界面设置里手动开启
-* 添加朗读快捷方式
-* 优化Epub解析 by hoodie13
-* epub书籍增加cache by hoodie13
-* 修复切换书籍或者章节时的断言崩溃问题。看漫画容易复现。 by hoodie13
-* 修正增加书签alert的正文内容较多时,确定键溢出屏幕问题 by hoodie13
-* 图片样式添加TEXT, 阅读界面菜单里可以选择图片样式
-
-**2021/02/26**
-
-* 添加反转内容功能
-* 更新章节时若无目录url将自动加载详情页
-* 添加变量nextChapterUrl
-* 订阅跳转外部应用时提示
-* 修复恢复bug
-* 详情页拼接url改为重定向后的地址
-* 不重复解析详情页
-
-**2021/02/21**
-
-* 下一页规则改为在内容规则之后执行
-* 书籍导出增加编码设置和导出文件夹设置,使用替换设置
-* 导入源添加等待框
-* 修复一些崩溃bug
-
-**2021/02/16**
-
-* 修复分享内容不对的bug
-* 优化主题颜色,添加透明度
-* rss分类url支持js
-* 打开阅读时同步阅读进度
-
-**2021/02/09**
-
-* 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低
-* 修复保存书源时不更新书源时间的bug
-* 订阅添加夜间模式,需启用js,还不是很完善
-* 优化源导入界面
-
-**2021/01/18**
-
-* 增加三星 S Pen 支持 by [dacer](https://github.com/dacer)
-* 订阅添加阅读下载,可以从多个渠道下载
-* 解决文件下载异常,在线语音可正常播放 by [Celeter](https://github.com/Celeter)
-* 更新默认在线朗读库, 默认id小于0方便下次更新时删除旧数据, 有重复的自己删除
-* 导入导出书单
-* 其它一些优化
-
-**2020/12/27**
-
-* 订阅添加搜索和分组
-* 修复部分手机状态栏bug
-* 单url订阅支持内容规则和样式
-
-**2020/12/09**
-
-* 修复bug
-* 优化中文排序
-* 优化编码识别
-* 选择文字时优先选词
-* 优化进度同步,进入书籍时同步,每次同步单本书,减少同步文件大小
-
-**2020/11/18**
-
-* 优化导航栏
-* js添加java.log(msg: String)用于调试时输出消息
-* js添加cookie变量,方法见io.legado.app.help.http.api.CookieManager
-* js添加cache变量,可以用来存储token之类的临时值,可以设置保存时间,方法见io.legado.app.help.CacheManager
-* 需要token的网站可以用js来写了,比如阿里tts
-
-**2020/11/15**
-
-* 正文规则添加字体规则,返回ByteArray
-* js添加方法:
-
-```
-base64DecodeToByteArray(str: String?): ByteArray?
-base64DecodeToByteArray(str: String?, flags: Int): ByteArray?
-```
-
-**2020/11/07**
-
-* 详情页菜单添加拷贝URL
-* 解决一些书名太长缓存报错的bug
-* 添加备份搜索记录
-* 替换编辑界面添加正则学习教程
-* 去除解析目录时拼接相对url,提升解析速度
-* 自动分段优化 by [tumuyan](https://github.com/tumuyan)
-* web支持图片显示 by [六月](https://github.com/Celeter)
-
-**2020/10/24**
-
-* 修复选择错误的bug
-* 修复长图最后一张不能滚动的bug
-* js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie
-  by [AndyBernie](https://github.com/AndyBernie)
-
-```
-java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd
-java.getCookie("http://baidu.com", "userid") => 1234
-```
-
-* 修复简繁转换没有处理标题
-* 每本书可以单独设置翻页动画,在菜单里
-* 添加重新分段功能,针对每本书,在菜单里,分段代码来自[tumuyan](https://github.com/tumuyan)
-
-**2020/10/07**
-
-* 更新时预下载10章
-* 支持更多分组
-* url添加js参数,解析url时执行,可在访问url时处理url,例
-
-```
-https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"}
-https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"}
-```
-
-**2020/09/29**
-
-* 增加了几个方法用于处理文件 by [Celeter](https://github.com/Celeter)
-
-```
-//文件下载,content为十六进制字符串,url用于生成文件名,返回文件路径
-downloadFile(content: String, url: String): String
-//文件解压,zipPath为压缩文件路径,返回解压路径
-unzipFile(zipPath: String): String
-//文件夹内所有文件读取
-getTxtInFolder(unzipPath: String): String
-```
-
-* 增加type字段,返回16进制字符串,例:`https://www.baidu.com,{"type":"zip"}`
-* 底部操作栏阴影跟随设置调节
+* 优化txt目录识别,取目录数量最多的规则

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
app/src/main/assets/web/bookshelf/css/detail.01ad55b1.css


+ 2 - 2
app/src/main/assets/web/bookshelf/index.html

@@ -1,3 +1,3 @@
-<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.54684f5f.css" rel="prefetch"><link href="css/detail.c24d2b34.css" rel="prefetch"><link href="js/about.56b5bbf2.js" rel="prefetch"><link href="js/detail.b1145ea3.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.5f0f4fba.css" rel="preload" as="style"><link href="js/app.f9143363.js" rel="preload" as="script"><link href="js/chunk-vendors.be6090cd.js" rel="preload" as="script"><link href="css/chunk-vendors.5f0f4fba.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
+<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.54684f5f.css" rel="prefetch"><link href="css/detail.01ad55b1.css" rel="prefetch"><link href="js/about.a288ed22.js" rel="prefetch"><link href="js/detail.562e75a6.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.5f0f4fba.css" rel="preload" as="style"><link href="js/app.cfea0936.js" rel="preload" as="script"><link href="js/chunk-vendors.be6090cd.js" rel="preload" as="script"><link href="css/chunk-vendors.5f0f4fba.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
         display: none;
-    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.be6090cd.js"></script><script src="js/app.f9143363.js"></script></body></html>
+    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.be6090cd.js"></script><script src="js/app.cfea0936.js"></script></body></html>

+ 0 - 0
app/src/main/assets/web/bookshelf/js/about.56b5bbf2.js → app/src/main/assets/web/bookshelf/js/about.a288ed22.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
app/src/main/assets/web/bookshelf/js/app.cfea0936.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
app/src/main/assets/web/bookshelf/js/detail.562e75a6.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
app/src/main/assets/web/bookshelf/js/detail.b1145ea3.js


+ 2 - 0
app/src/main/java/io/legado/app/App.kt

@@ -14,6 +14,7 @@ import io.legado.app.constant.AppConst.channelIdReadAloud
 import io.legado.app.constant.AppConst.channelIdWeb
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
+import io.legado.app.help.BookHelp
 import io.legado.app.help.CrashHandler
 import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.RuleBigDataHelp
@@ -52,6 +53,7 @@ class App : MultiDexApplication() {
                 appDb.searchBookDao.clearExpired(clearTime)
             }
             RuleBigDataHelp.clearInvalid()
+            BookHelp.clearInvalidCache()
             //初始化简繁转换引擎
             when (AppConfig.chineseConverterType) {
                 1 -> ChineseUtils.t2s("初始化")

+ 18 - 24
app/src/main/java/io/legado/app/api/controller/BookSourceController.kt

@@ -7,7 +7,6 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonArray
-import io.legado.app.utils.msg
 
 object BookSourceController {
 
@@ -23,20 +22,16 @@ object BookSourceController {
     fun saveSource(postData: String?): ReturnData {
         val returnData = ReturnData()
         postData ?: return returnData.setErrorMsg("数据不能为空")
-        kotlin.runCatching {
-            val bookSource = BookSource.fromJson(postData)
-            if (bookSource != null) {
-                if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
-                    returnData.setErrorMsg("源名称和URL不能为空")
-                } else {
-                    appDb.bookSourceDao.insert(bookSource)
-                    returnData.setData("")
-                }
+        val bookSource = BookSource.fromJson(postData).getOrNull()
+        if (bookSource != null) {
+            if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
+                returnData.setErrorMsg("源名称和URL不能为空")
             } else {
-                returnData.setErrorMsg("转换源失败")
+                appDb.bookSourceDao.insert(bookSource)
+                returnData.setData("")
             }
-        }.onFailure {
-            returnData.setErrorMsg(it.msg)
+        } else {
+            returnData.setErrorMsg("转换源失败")
         }
         return returnData
     }
@@ -44,19 +39,18 @@ object BookSourceController {
     fun saveSources(postData: String?): ReturnData {
         postData ?: return ReturnData().setErrorMsg("数据为空")
         val okSources = arrayListOf<BookSource>()
-        val bookSources = BookSource.fromJsonArray(postData)
-        if (bookSources.isNotEmpty()) {
-            bookSources.forEach { bookSource ->
-                if (bookSource.bookSourceName.isNotBlank()
-                    && bookSource.bookSourceUrl.isNotBlank()
-                ) {
-                    appDb.bookSourceDao.insert(bookSource)
-                    okSources.add(bookSource)
-                }
-            }
-        } else {
+        val bookSources = BookSource.fromJsonArray(postData).getOrNull()
+        if (bookSources.isNullOrEmpty()) {
             return ReturnData().setErrorMsg("转换源失败")
         }
+        bookSources.forEach { bookSource ->
+            if (bookSource.bookSourceName.isNotBlank()
+                && bookSource.bookSourceUrl.isNotBlank()
+            ) {
+                appDb.bookSourceDao.insert(bookSource)
+                okSources.add(bookSource)
+            }
+        }
         return ReturnData().setData(okSources)
     }
 

+ 21 - 28
app/src/main/java/io/legado/app/api/controller/RssSourceController.kt

@@ -5,7 +5,6 @@ import android.text.TextUtils
 import io.legado.app.api.ReturnData
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.RssSource
-import io.legado.app.utils.msg
 
 object RssSourceController {
 
@@ -21,20 +20,15 @@ object RssSourceController {
     fun saveSource(postData: String?): ReturnData {
         val returnData = ReturnData()
         postData ?: return returnData.setErrorMsg("数据不能为空")
-        kotlin.runCatching {
-            val source = RssSource.fromJson(postData)
-            if (source != null) {
-                if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
-                    returnData.setErrorMsg("源名称和URL不能为空")
-                } else {
-                    appDb.rssSourceDao.insert(source)
-                    returnData.setData("")
-                }
+        RssSource.fromJson(postData).onFailure {
+            returnData.setErrorMsg("转换源失败${it.localizedMessage}")
+        }.onSuccess { source ->
+            if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
+                returnData.setErrorMsg("源名称和URL不能为空")
             } else {
-                returnData.setErrorMsg("转换源失败")
+                appDb.rssSourceDao.insert(source)
+                returnData.setData("")
             }
-        }.onFailure {
-            returnData.setErrorMsg(it.msg)
         }
         return returnData
     }
@@ -42,18 +36,17 @@ object RssSourceController {
     fun saveSources(postData: String?): ReturnData {
         postData ?: return ReturnData().setErrorMsg("数据不能为空")
         val okSources = arrayListOf<RssSource>()
-        val source = RssSource.fromJsonArray(postData)
-        if (source.isNotEmpty()) {
-            for (rssSource in source) {
-                if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
-                    continue
-                }
-                appDb.rssSourceDao.insert(rssSource)
-                okSources.add(rssSource)
-            }
-        } else {
+        val source = RssSource.fromJsonArray(postData).getOrNull()
+        if (source.isNullOrEmpty()) {
             return ReturnData().setErrorMsg("转换源失败")
         }
+        for (rssSource in source) {
+            if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
+                continue
+            }
+            appDb.rssSourceDao.insert(rssSource)
+            okSources.add(rssSource)
+        }
         return ReturnData().setData(okSources)
     }
 
@@ -70,11 +63,11 @@ object RssSourceController {
 
     fun deleteSources(postData: String?): ReturnData {
         postData ?: return ReturnData().setErrorMsg("没有传递数据")
-        kotlin.runCatching {
-            RssSource.fromJsonArray(postData).let {
-                it.forEach { source ->
-                    appDb.rssSourceDao.delete(source)
-                }
+        RssSource.fromJsonArray(postData).onFailure {
+            return ReturnData().setErrorMsg("格式不对")
+        }.onSuccess {
+            it.forEach { source ->
+                appDb.rssSourceDao.delete(source)
             }
         }
         return ReturnData().setData("已执行"/*okSources*/)

+ 1 - 1
app/src/main/java/io/legado/app/constant/AppPattern.kt

@@ -22,7 +22,7 @@ object AppPattern {
     val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
 
     //本地书籍支持类型
-    val bookFileRegex = Regex("(?i).*\\.(txt|epub|umd)")
+    val bookFileRegex = Regex(".*\\.(txt|epub|umd)", RegexOption.IGNORE_CASE)
 
     /**
      * 所有标点

+ 21 - 7
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -12,15 +12,26 @@ interface BookSourceDao {
 
     @Query(
         """select * from book_sources 
-        where bookSourceName like :searchKey 
-        or bookSourceGroup like :searchKey 
-        or bookSourceUrl like :searchKey  
-        or bookSourceComment like :searchKey 
+        where bookSourceName like '%' || :searchKey || '%'
+        or bookSourceGroup like '%' || :searchKey || '%'
+        or bookSourceUrl like '%' || :searchKey || '%'
+        or bookSourceComment like '%' || :searchKey || '%' 
         order by customOrder asc"""
     )
     fun flowSearch(searchKey: String): Flow<List<BookSource>>
 
-    @Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
+    @Query(
+        """select * from book_sources 
+        where enabled = 1 and 
+        (bookSourceName like '%' || :searchKey || '%' 
+        or bookSourceGroup like '%' || :searchKey || '%' 
+        or bookSourceUrl like '%' || :searchKey || '%'  
+        or bookSourceComment like '%' || :searchKey || '%')
+        order by customOrder asc"""
+    )
+    fun flowSearchEnabled(searchKey: String): Flow<List<BookSource>>
+
+    @Query("select * from book_sources where bookSourceGroup like '%' || :searchKey || '%' order by customOrder asc")
     fun flowGroupSearch(searchKey: String): Flow<List<BookSource>>
 
     @Query("select * from book_sources where enabled = 1 order by customOrder asc")
@@ -39,7 +50,7 @@ interface BookSourceDao {
         """select * from book_sources 
         where enabledExplore = 1 
         and trim(exploreUrl) <> '' 
-        and (bookSourceGroup like :key or bookSourceName like :key) 
+        and (bookSourceGroup like '%' || :key || '%' or bookSourceName like '%' || :key || '%') 
         order by customOrder asc"""
     )
     fun flowExplore(key: String): Flow<List<BookSource>>
@@ -48,7 +59,7 @@ interface BookSourceDao {
         """select * from book_sources 
         where enabledExplore = 1 
         and trim(exploreUrl) <> '' 
-        and (bookSourceGroup like :key) 
+        and (bookSourceGroup like '%' || :key || '%') 
         order by customOrder asc"""
     )
     fun flowGroupExplore(key: String): Flow<List<BookSource>>
@@ -74,6 +85,9 @@ interface BookSourceDao {
     @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
     fun getEnabledByGroup(group: String): List<BookSource>
 
+    @Query("select * from book_sources where enabled = 1 and bookSourceType = :type")
+    fun getEnabledByType(type: Int): List<BookSource>
+
     @get:Query("select * from book_sources where trim(bookUrlPattern) <> '' order by enabled desc, customOrder")
     val hasBookUrlPattern: List<BookSource>
 

+ 5 - 1
app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt

@@ -7,7 +7,11 @@ import io.legado.app.data.entities.Bookmark
 @Dao
 interface BookmarkDao {
 
-    @get:Query("select * from bookmarks")
+    @get:Query(
+        """
+        select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos
+    """
+    )
     val all: List<Bookmark>
 
     @Query(

+ 20 - 25
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -6,6 +6,8 @@ import io.legado.app.constant.AppPattern
 import io.legado.app.constant.BookType
 import io.legado.app.constant.PageAnim
 import io.legado.app.data.appDb
+import io.legado.app.help.BookHelp
+import io.legado.app.help.ContentProcessor
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadBook
@@ -271,7 +273,13 @@ data class Book(
         this.tocHtml = this@Book.tocHtml
     }
 
-    fun changeTo(newBook: Book) {
+    fun changeTo(newBook: Book, toc: List<BookChapter>): Book {
+        newBook.durChapterIndex = BookHelp
+            .getDurChapter(durChapterIndex, durChapterTitle, toc, totalChapterNum)
+        newBook.durChapterTitle = toc[newBook.durChapterIndex].getDisplayTitle(
+            ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules()
+        )
+        newBook.durChapterPos = durChapterPos
         newBook.group = group
         newBook.order = order
         newBook.customCoverUrl = customCoverUrl
@@ -279,23 +287,11 @@ data class Book(
         newBook.customTag = customTag
         newBook.canUpdate = canUpdate
         newBook.readConfig = readConfig
-        delete(this)
-        appDb.bookDao.insert(newBook)
-    }
-
-    fun upInfoFromOld(oldBook: Book?) {
-        oldBook?.let {
-            group = oldBook.group
-            durChapterIndex = oldBook.durChapterIndex
-            durChapterPos = oldBook.durChapterPos
-            durChapterTitle = oldBook.durChapterTitle
-            customCoverUrl = oldBook.customCoverUrl
-            customIntro = oldBook.customIntro
-            order = oldBook.order
-            if (coverUrl.isNullOrEmpty()) {
-                coverUrl = oldBook.getDisplayCover()
-            }
+        if (appDb.bookDao.has(bookUrl) == true) {
+            delete()
+            appDb.bookDao.insert(newBook)
         }
+        return newBook
     }
 
     fun createBookMark(): Bookmark {
@@ -313,20 +309,19 @@ data class Book(
         }
     }
 
+    fun delete() {
+        if (ReadBook.book?.bookUrl == bookUrl) {
+            ReadBook.book = null
+        }
+        appDb.bookDao.delete(this)
+    }
+
     companion object {
         const val hTag = 2L
         const val rubyTag = 4L
         const val imgStyleDefault = "DEFAULT"
         const val imgStyleFull = "FULL"
         const val imgStyleText = "TEXT"
-
-        fun delete(book: Book?) {
-            book ?: return
-            if (ReadBook.book?.bookUrl == book.bookUrl) {
-                ReadBook.book = null
-            }
-            appDb.bookDao.delete(book)
-        }
     }
 
     @Parcelize

+ 7 - 2
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -11,6 +11,7 @@ import io.legado.app.utils.*
 import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
 import splitties.init.appCtx
+import java.io.InputStream
 
 @Parcelize
 @TypeConverters(BookSource.Converters::class)
@@ -203,13 +204,17 @@ data class BookSource(
 
     companion object {
 
-        fun fromJson(json: String): BookSource? {
+        fun fromJson(json: String): Result<BookSource> {
             return SourceAnalyzer.jsonToBookSource(json)
         }
 
-        fun fromJsonArray(json: String): List<BookSource> {
+        fun fromJsonArray(json: String): Result<MutableList<BookSource>> {
             return SourceAnalyzer.jsonToBookSources(json)
         }
+
+        fun fromJsonArray(inputStream: InputStream): Result<MutableList<BookSource>> {
+            return SourceAnalyzer.jsonToBookSources(inputStream)
+        }
     }
 
     class Converters {

+ 13 - 11
app/src/main/java/io/legado/app/data/entities/RssSource.kt

@@ -121,7 +121,7 @@ data class RssSource(
     @Suppress("MemberVisibilityCanBePrivate")
     companion object {
 
-        fun fromJsonDoc(doc: DocumentContext): RssSource? {
+        fun fromJsonDoc(doc: DocumentContext): Result<RssSource> {
             return kotlin.runCatching {
                 val loginUi = doc.read<Any>("$.loginUi")
                 RssSource(
@@ -152,23 +152,25 @@ data class RssSource(
                     loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
                     customOrder = doc.readInt("$.customOrder") ?: 0
                 )
-            }.getOrNull()
+            }
         }
 
-        fun fromJson(json: String): RssSource? {
+        fun fromJson(json: String): Result<RssSource> {
             return fromJsonDoc(jsonPath.parse(json))
         }
 
-        fun fromJsonArray(jsonArray: String): ArrayList<RssSource> {
-            val sources = arrayListOf<RssSource>()
-            val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
-            doc.forEach {
-                val jsonItem = jsonPath.parse(it)
-                fromJsonDoc(jsonItem)?.let { source ->
-                    sources.add(source)
+        fun fromJsonArray(jsonArray: String): Result<ArrayList<RssSource>> {
+            return kotlin.runCatching {
+                val sources = arrayListOf<RssSource>()
+                val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
+                doc.forEach {
+                    val jsonItem = jsonPath.parse(it)
+                    fromJsonDoc(jsonItem).getOrThrow().let { source ->
+                        sources.add(source)
+                    }
                 }
+                sources
             }
-            return sources
         }
     }
 

+ 6 - 10
app/src/main/java/io/legado/app/help/BookHelp.kt

@@ -6,14 +6,11 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
-import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers.IO
 import org.apache.commons.text.similarity.JaccardSimilarity
 import splitties.init.appCtx
 import java.io.File
@@ -44,11 +41,10 @@ object BookHelp {
     /**
      * 清除已删除书的缓存
      */
-    fun clearRemovedCache() {
-        Coroutine.async {
-            val bookFolderNames = arrayListOf<String>()
-            appDb.bookDao.all.forEach {
-                bookFolderNames.add(it.getFolderName())
+    suspend fun clearInvalidCache() {
+        withContext(IO) {
+            val bookFolderNames = appDb.bookDao.all.map {
+                it.getFolderName()
             }
             val file = downloadDir.getFile(cacheFolderName)
             file.listFiles()?.forEach { bookFile ->

+ 5 - 7
app/src/main/java/io/legado/app/help/DefaultData.kt

@@ -54,13 +54,11 @@ object DefaultData {
     }
 
     val rssSources: List<RssSource> by lazy {
-        kotlin.runCatching {
-            val json = String(
-                appCtx.assets.open("defaultData${File.separator}rssSources.json")
-                    .readBytes()
-            )
-            RssSource.fromJsonArray(json)
-        }.getOrDefault(emptyList())
+        val json = String(
+            appCtx.assets.open("defaultData${File.separator}rssSources.json")
+                .readBytes()
+        )
+        RssSource.fromJsonArray(json).getOrDefault(emptyList())
     }
 
     val coverRuleConfig: BookCover.CoverRuleConfig by lazy {

+ 1 - 1
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -139,7 +139,7 @@ interface JsExtensions {
             path.startsWith("/storage") -> FileUtils.readText(path)
             else -> readTxtFile(path)
         }
-        if (result.isBlank()) throw NoStackTraceException("${path} 内容获取失败或者为空")
+        if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空")
         return result
     }
 

+ 48 - 13
app/src/main/java/io/legado/app/help/SourceAnalyzer.kt

@@ -7,7 +7,9 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.BookType
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.rule.*
+import io.legado.app.exception.NoStackTraceException
 import io.legado.app.utils.*
+import java.io.InputStream
 
 import java.util.regex.Pattern
 
@@ -16,31 +18,66 @@ object SourceAnalyzer {
     private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE)
     private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
 
-    fun jsonToBookSources(json: String): List<BookSource> {
-        val bookSources = mutableListOf<BookSource>()
-        if (json.isJsonArray()) {
-            val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
-            for (item in items) {
+    fun jsonToBookSources(json: String): Result<MutableList<BookSource>> {
+        return kotlin.runCatching {
+            val bookSources = mutableListOf<BookSource>()
+            when {
+                json.isJsonArray() -> {
+                    val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
+                    for (item in items) {
+                        val jsonItem = jsonPath.parse(item)
+                        jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
+                            bookSources.add(it)
+                        }
+                    }
+                }
+                json.isJsonObject() -> {
+                    jsonToBookSource(json).getOrThrow().let {
+                        bookSources.add(it)
+                    }
+                }
+                else -> {
+                    throw NoStackTraceException("格式不对")
+                }
+            }
+            bookSources
+        }
+    }
+
+    fun jsonToBookSources(inputStream: InputStream): Result<MutableList<BookSource>> {
+        return kotlin.runCatching {
+            val bookSources = mutableListOf<BookSource>()
+            kotlin.runCatching {
+                val items: List<Map<String, Any>> = jsonPath.parse(inputStream).read("$")
+                for (item in items) {
+                    val jsonItem = jsonPath.parse(item)
+                    jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
+                        bookSources.add(it)
+                    }
+                }
+            }.onFailure {
+                val item: Map<String, Any> = jsonPath.parse(inputStream).read("$")
                 val jsonItem = jsonPath.parse(item)
-                jsonToBookSource(jsonItem.jsonString())?.let {
+                jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
                     bookSources.add(it)
                 }
             }
+            bookSources
         }
-        return bookSources
     }
 
-    fun jsonToBookSource(json: String): BookSource? {
+    fun jsonToBookSource(json: String): Result<BookSource> {
         val source = BookSource()
         val sourceAny = GSON.fromJsonObject<BookSourceAny>(json.trim())
             .onFailure {
                 AppLog.put("转化书源出错", it)
             }.getOrNull()
-        try {
+        return kotlin.runCatching {
             if (sourceAny?.ruleToc == null) {
                 source.apply {
                     val jsonItem = jsonPath.parse(json.trim())
-                    bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: return null
+                    bookSourceUrl = jsonItem.readString("bookSourceUrl")
+                        ?: throw NoStackTraceException("格式不对")
                     bookSourceName = jsonItem.readString("bookSourceName") ?: ""
                     bookSourceGroup = jsonItem.readString("bookSourceGroup")
                     loginUrl = jsonItem.readString("loginUrl")
@@ -168,10 +205,8 @@ object SourceAnalyzer {
                         .getOrNull()
                 }
             }
-        } catch (e: Exception) {
-            e.printOnDebug()
+            source
         }
-        return source
     }
 
     @Keep

+ 6 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -301,6 +301,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
     val doublePageHorizontal: Boolean
         get() = appCtx.getPrefBoolean(PreferKey.doublePageHorizontal, true)
 
+    var searchGroup: String
+        get() = appCtx.getPrefString("searchGroup") ?: ""
+        set(value) {
+            appCtx.putPrefString("searchGroup", value)
+        }
+
     private fun getPrefUserAgent(): String {
         val ua = appCtx.getPrefString(PreferKey.userAgent)
         if (ua.isNullOrBlank()) {

+ 4 - 3
app/src/main/java/io/legado/app/help/storage/ImportOldData.kt

@@ -91,9 +91,10 @@ object ImportOldData {
     }
 
     fun importOldSource(json: String): Int {
-        val bookSources = BookSource.fromJsonArray(json)
-        appDb.bookSourceDao.insert(*bookSources.toTypedArray())
-        return bookSources.size
+        val count = BookSource.fromJsonArray(json).onSuccess {
+            appDb.bookSourceDao.insert(*it.toTypedArray())
+        }.getOrNull()?.size
+        return count ?: 0
     }
 
     private fun importOldReplaceRule(json: String): Int {

+ 4 - 2
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -22,6 +22,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import java.io.File
+import java.io.FileInputStream
 
 
 object Restore {
@@ -192,8 +193,9 @@ object Restore {
     private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
         try {
             val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
-            val json = file.readText()
-            return GSON.fromJsonArray<T>(json).getOrThrow()
+            FileInputStream(file).use {
+                return GSON.fromJsonArray<T>(it).getOrThrow()
+            }
         } catch (e: Exception) {
             AppLog.put("$fileName\n读取解析出错\n${e.localizedMessage}", e)
             appCtx.toastOnUi("$fileName\n读取文件出错\n${e.localizedMessage}")

+ 2 - 2
app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt

@@ -48,9 +48,9 @@ fun Context.alert(
 
 inline fun Fragment.alert(
     titleResource: Int? = null,
-    message: Int? = null,
+    messageResource: Int? = null,
     noinline init: (AlertBuilder<DialogInterface>.() -> Unit)? = null
-) = requireActivity().alert(titleResource, message, init)
+) = requireActivity().alert(titleResource, messageResource, init)
 
 fun Context.alert(init: AlertBuilder<AlertDialog>.() -> Unit): AlertDialog =
     AndroidAlertBuilder(this).apply {

+ 3 - 0
app/src/main/java/io/legado/app/lib/webdav/WebDav.kt

@@ -1,5 +1,6 @@
 package io.legado.app.lib.webdav
 
+import io.legado.app.constant.AppLog
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.text
@@ -168,6 +169,8 @@ class WebDav(urlStr: String) {
                         addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
                     }.close()
                 }
+            }.onFailure {
+                AppLog.put(it.localizedMessage)
             }.isSuccess
         }
         return false

+ 1 - 0
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -22,6 +22,7 @@ import javax.script.SimpleBindings
 object LocalBook {
 
     private val nameAuthorPatterns = arrayOf(
+        Pattern.compile("(.*?)《([^《》]+)》.*?作者:(.*)"),
         Pattern.compile("(.*?)《([^《》]+)》(.*)"),
         Pattern.compile("(^)(.+) 作者:(.+)$"),
         Pattern.compile("(^)(.+) by (.+)$")

+ 2 - 2
app/src/main/java/io/legado/app/model/webBook/SearchModel.kt

@@ -8,7 +8,6 @@ import io.legado.app.data.entities.SearchBook
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.utils.getPrefBoolean
-import io.legado.app.utils.getPrefString
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExecutorCoroutineDispatcher
 import kotlinx.coroutines.asCoroutineDispatcher
@@ -60,7 +59,7 @@ class SearchModel(private val scope: CoroutineScope) {
             initSearchPool()
             mSearchId = searchId
             searchPage = 1
-            val searchGroup = appCtx.getPrefString("searchGroup") ?: ""
+            val searchGroup = AppConfig.searchGroup
             bookSourceList.clear()
             searchBooks.clear()
             callBack?.onSearchSuccess(searchBooks)
@@ -69,6 +68,7 @@ class SearchModel(private val scope: CoroutineScope) {
             } else {
                 val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
                 if (sources.isEmpty()) {
+                    AppConfig.searchGroup = ""
                     bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
                 } else {
                     bookSourceList.addAll(sources)

+ 23 - 20
app/src/main/java/io/legado/app/model/webBook/WebBook.kt

@@ -262,7 +262,7 @@ object WebBook {
             Debug.log(bookSource.bookSourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
             return bookChapter.url
         }
-        if(bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {
+        if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {
             Debug.log(bookSource.bookSourceUrl, "⇒一级目录正文不解析规则")
             return bookChapter.tag ?: ""
         }
@@ -320,35 +320,38 @@ object WebBook {
         name: String,
         author: String,
         context: CoroutineContext = Dispatchers.IO,
-    ): Coroutine<Pair<BookSource, Book>> {
+    ): Coroutine<Pair<Book, BookSource>> {
         return Coroutine.async(scope, context) {
-            preciseSearchAwait(scope, bookSources, name, author)
-                ?: throw NoStackTraceException("没有搜索到<$name>$author")
+            for (source in bookSources) {
+                val book = preciseSearchAwait(scope, source, name, author).getOrNull()
+                if (book != null) {
+                    return@async Pair(book, source)
+                }
+            }
+            throw NoStackTraceException("没有搜索到<$name>$author")
         }
     }
 
     suspend fun preciseSearchAwait(
         scope: CoroutineScope,
-        bookSources: List<BookSource>,
+        bookSource: BookSource,
         name: String,
-        author: String
-    ): Pair<BookSource, Book>? {
-        bookSources.forEach { source ->
-            kotlin.runCatching {
-                if (!scope.isActive) return null
-                searchBookAwait(scope, source, name).firstOrNull {
-                    it.name == name && it.author == author
-                }?.let { searchBook ->
-                    if (!scope.isActive) return null
-                    var book = searchBook.toBook()
-                    if (book.tocUrl.isBlank()) {
-                        book = getBookInfoAwait(scope, source, book)
-                    }
-                    return Pair(source, book)
+        author: String,
+    ): Result<Book?> {
+        return kotlin.runCatching {
+            if (!scope.isActive) return@runCatching null
+            searchBookAwait(scope, bookSource, name).firstOrNull {
+                it.name == name && it.author == author
+            }?.let { searchBook ->
+                if (!scope.isActive) return@runCatching null
+                var book = searchBook.toBook()
+                if (book.tocUrl.isBlank()) {
+                    book = getBookInfoAwait(scope, bookSource, book)
                 }
+                return@runCatching book
             }
+            return@runCatching null
         }
-        return null
     }
 
 }

+ 8 - 7
app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt

@@ -31,9 +31,8 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
                 } else {
                     DocumentFile.fromSingleUri(context, uri)?.readText(context)
                 } ?: throw NoStackTraceException("文件不存在")
-                if (content.isJson()) {
-                    //暂时根据文件内容判断属于什么
-                    when {
+                when {
+                    content.isJson() -> when {
                         content.contains("bookSourceUrl") ->
                             importBookSourceLive.postValue(content)
                         content.contains("sourceUrl") ->
@@ -48,10 +47,12 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
                             importHttpTTS(content, finally)
                         else -> errorLiveData.postValue("格式不对")
                     }
-                } else if (uri.toString().matches(bookFileRegex)) {
-                    importBookLiveData.postValue(uri)
-                } else {
-                    throw NoStackTraceException("暂未支持的本地书籍格式(TXT/UMD/EPUB)")
+                    (uri.path ?: uri.toString()).matches(bookFileRegex) -> {
+                        importBookLiveData.postValue(uri)
+                    }
+                    else -> {
+                        throw NoStackTraceException("暂未支持的本地书籍格式(TXT/UMD/EPUB)")
+                    }
                 }
             } else {
                 onLineImportLive.postValue(uri)

+ 1 - 1
app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt

@@ -189,7 +189,7 @@ class ImportBookSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_vie
 
     override fun onCodeSave(code: String, requestId: String?) {
         requestId?.toInt()?.let {
-            BookSource.fromJson(code)?.let { source ->
+            BookSource.fromJson(code).getOrNull()?.let { source ->
                 viewModel.allSources[it] = source
                 adapter.setItem(it, source)
             }

+ 4 - 24
app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt

@@ -14,7 +14,6 @@ import io.legado.app.help.SourceHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
-import io.legado.app.help.http.text
 import io.legado.app.utils.*
 
 
@@ -98,13 +97,12 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
                             importSourceUrl(it)
                         }
                     } else {
-                        BookSource.fromJson(mText)?.let {
+                        BookSource.fromJson(mText).getOrThrow().let {
                             allSources.add(it)
                         }
                     }
                 }
-                mText.isJsonArray() -> {
-                    val items = BookSource.fromJsonArray(mText)
+                mText.isJsonArray() -> BookSource.fromJsonArray(mText).getOrThrow().let { items ->
                     allSources.addAll(items)
                 }
                 mText.isAbsUrl() -> {
@@ -123,26 +121,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
     private suspend fun importSourceUrl(url: String) {
         okHttpClient.newCallResponseBody {
             url(url)
-        }.text("utf-8").let { body ->
-            when {
-                body.isJsonArray() -> {
-                    val items: List<Map<String, Any>> = jsonPath.parse(body).read("$")
-                    for (item in items) {
-                        val jsonItem = jsonPath.parse(item)
-                        BookSource.fromJson(jsonItem.jsonString())?.let { source ->
-                            allSources.add(source)
-                        }
-                    }
-                }
-                body.isJsonObject() -> {
-                    BookSource.fromJson(body)?.let {
-                        allSources.add(it)
-                    }
-                }
-                else -> {
-                    throw NoStackTraceException(context.getString(R.string.wrong_format))
-                }
-            }
+        }.byteStream().let {
+            allSources.addAll(BookSource.fromJsonArray(it).getOrThrow())
         }
     }
 

+ 5 - 5
app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt

@@ -92,14 +92,14 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
             adapter.notifyDataSetChanged()
             upSelectText()
         }
-        viewModel.errorLiveData.observe(this, {
+        viewModel.errorLiveData.observe(this) {
             binding.rotateLoading.hide()
             binding.tvMsg.apply {
                 text = it
                 visible()
             }
-        })
-        viewModel.successLiveData.observe(this, {
+        }
+        viewModel.successLiveData.observe(this) {
             binding.rotateLoading.hide()
             if (it > 0) {
                 adapter.setItems(viewModel.allSources)
@@ -110,7 +110,7 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
                     visible()
                 }
             }
-        })
+        }
         val source = arguments?.getString("source")
         if (source.isNullOrEmpty()) {
             dismiss()
@@ -188,7 +188,7 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
 
     override fun onCodeSave(code: String, requestId: String?) {
         requestId?.toInt()?.let {
-            RssSource.fromJson(code)?.let { source ->
+            RssSource.fromJson(code).getOrNull()?.let { source ->
                 viewModel.allSources[it] = source
                 adapter.setItem(it, source)
             }

+ 4 - 5
app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt

@@ -13,7 +13,6 @@ import io.legado.app.help.SourceHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
-import io.legado.app.help.http.text
 import io.legado.app.utils.*
 
 class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
@@ -95,7 +94,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
                             importSourceUrl(it)
                         }
                     } else {
-                        RssSource.fromJsonArray(mText).let {
+                        RssSource.fromJsonArray(mText).getOrThrow().let {
                             allSources.addAll(it)
                         }
                     }
@@ -104,7 +103,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
                     val items: List<Map<String, Any>> = jsonPath.parse(mText).read("$")
                     for (item in items) {
                         val jsonItem = jsonPath.parse(item)
-                        RssSource.fromJsonDoc(jsonItem)?.let {
+                        RssSource.fromJsonDoc(jsonItem).getOrThrow().let {
                             allSources.add(it)
                         }
                     }
@@ -124,11 +123,11 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
     private suspend fun importSourceUrl(url: String) {
         okHttpClient.newCallResponseBody {
             url(url)
-        }.text("utf-8").let { body ->
+        }.byteStream().let { body ->
             val items: List<Map<String, Any>> = jsonPath.parse(body).read("$")
             for (item in items) {
                 val jsonItem = jsonPath.parse(item)
-                RssSource.fromJson(jsonItem.jsonString())?.let { source ->
+                RssSource.fromJson(jsonItem.jsonString()).getOrThrow().let { source ->
                     allSources.add(source)
                 }
             }

+ 20 - 0
app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt

@@ -15,11 +15,13 @@ import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookGroup
+import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityArrangeBookBinding
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.book.group.GroupManageDialog
 import io.legado.app.ui.book.group.GroupSelectDialog
+import io.legado.app.ui.theme.AppTheme
 import io.legado.app.ui.widget.SelectActionBar
 import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
@@ -41,6 +43,7 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
     PopupMenu.OnMenuItemClickListener,
     SelectActionBar.CallBack,
     ArrangeBookAdapter.CallBack,
+    SourcePickerDialog.Callback,
     GroupSelectDialog.CallBack {
 
     override val binding by viewBinding(ActivityArrangeBookBinding::inflate)
@@ -107,6 +110,17 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
         binding.selectActionBar.inflateMenu(R.menu.arrange_book_sel)
         binding.selectActionBar.setOnMenuItemClickListener(this)
         binding.selectActionBar.setCallBack(this)
+        binding.composeView.setContent {
+            AppTheme {
+                BatchChangeSourceDialog(
+                    state = viewModel.batchChangeSourceState,
+                    size = viewModel.batchChangeSourceSize,
+                    position = viewModel.batchChangeSourcePosition
+                ) {
+                    viewModel.batchChangeSourceCoroutine?.cancel()
+                }
+            }
+        }
     }
 
     @SuppressLint("NotifyDataSetChanged")
@@ -169,6 +183,7 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
             R.id.menu_update_disable ->
                 viewModel.upCanUpdate(adapter.selectedBooks(), false)
             R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0)
+            R.id.menu_change_source -> showDialogFragment<SourcePickerDialog>()
         }
         return false
     }
@@ -228,4 +243,9 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
         }
     }
 
+    override fun sourceOnClick(source: BookSource) {
+        viewModel.changeSource(adapter.selectedBooks(), source)
+        viewModel.batchChangeSourceState.value = true
+    }
+
 }

+ 29 - 0
app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt

@@ -1,13 +1,22 @@
 package io.legado.app.ui.book.arrange
 
 import android.app.Application
+import androidx.compose.runtime.mutableStateOf
 import io.legado.app.base.BaseViewModel
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
+import io.legado.app.help.coroutine.Coroutine
+import io.legado.app.model.webBook.WebBook
 
 
 class ArrangeBookViewModel(application: Application) : BaseViewModel(application) {
 
+    val batchChangeSourceState = mutableStateOf(false)
+    val batchChangeSourceSize = mutableStateOf(0)
+    val batchChangeSourcePosition = mutableStateOf(0)
+    var batchChangeSourceCoroutine: Coroutine<Unit>? = null
+
     fun upCanUpdate(books: Array<Book>, canUpdate: Boolean) {
         execute {
             books.forEach {
@@ -29,4 +38,24 @@ class ArrangeBookViewModel(application: Application) : BaseViewModel(application
         }
     }
 
+    fun changeSource(books: Array<Book>, source: BookSource) {
+        batchChangeSourceCoroutine?.cancel()
+        batchChangeSourceCoroutine = execute {
+            batchChangeSourceSize.value = books.size
+            books.forEachIndexed { index, book ->
+                batchChangeSourcePosition.value = index + 1
+                if (book.isLocalBook()) return@forEachIndexed
+                if (book.origin == source.bookSourceUrl) return@forEachIndexed
+                WebBook.preciseSearchAwait(this, source, book.name, book.author)
+                    .getOrNull()?.let { newBook ->
+                        val toc = WebBook.getChapterListAwait(this, source, newBook)
+                        book.changeTo(newBook, toc)
+                        appDb.bookChapterDao.insert(*toc.toTypedArray())
+                    }
+            }
+        }.onFinally {
+            batchChangeSourceState.value = false
+        }
+    }
+
 }

+ 45 - 0
app/src/main/java/io/legado/app/ui/book/arrange/ComposeView.kt

@@ -0,0 +1,45 @@
+package io.legado.app.ui.book.arrange
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.LinearProgressIndicator
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
+import io.legado.app.R
+import splitties.init.appCtx
+
+@Composable
+fun BatchChangeSourceDialog(
+    state: MutableState<Boolean>,
+    size: MutableState<Int>,
+    position: MutableState<Int>,
+    cancel: () -> Unit
+) {
+    if (state.value) {
+        AlertDialog(
+            onDismissRequest = { },
+            confirmButton = {
+                TextButton(onClick = {
+                    cancel.invoke()
+                    state.value = false
+                }, content = {
+                    Text(text = "取消")
+                })
+            },
+            title = {
+                Text(text = appCtx.getString(R.string.change_source_batch))
+            },
+            text = {
+                Column(horizontalAlignment = Alignment.CenterHorizontally) {
+                    Text(text = "${position.value}/${size.value}")
+                    LinearProgressIndicator(
+                        progress = position.value / size.value.toFloat()
+                    )
+                }
+            }
+        )
+    }
+}

+ 121 - 0
app/src/main/java/io/legado/app/ui/book/arrange/SourcePickerDialog.kt

@@ -0,0 +1,121 @@
+package io.legado.app.ui.book.arrange
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.widget.SearchView
+import androidx.core.view.setPadding
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.legado.app.R
+import io.legado.app.base.BaseDialogFragment
+import io.legado.app.base.adapter.ItemViewHolder
+import io.legado.app.base.adapter.RecyclerAdapter
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.BookSource
+import io.legado.app.databinding.DialogSourcePickerBinding
+import io.legado.app.databinding.Item1lineTextBinding
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.lib.theme.primaryTextColor
+import io.legado.app.utils.applyTint
+import io.legado.app.utils.dpToPx
+import io.legado.app.utils.setLayout
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import splitties.views.onClick
+
+class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) {
+
+    private val binding by viewBinding(DialogSourcePickerBinding::bind)
+    private val searchView: SearchView by lazy {
+        binding.toolBar.findViewById(R.id.search_view)
+    }
+    private val adapter by lazy {
+        SourceAdapter(requireContext())
+    }
+    private var sourceFlowJob: Job? = null
+
+    override fun onStart() {
+        super.onStart()
+        setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT)
+    }
+
+    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
+        initView()
+        initData()
+    }
+
+    private fun initView() {
+        binding.toolBar.setBackgroundColor(primaryColor)
+        binding.toolBar.title = "选择书源"
+        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
+        binding.recyclerView.adapter = adapter
+        searchView.applyTint(primaryTextColor)
+        searchView.onActionViewExpanded()
+        searchView.isSubmitButtonEnabled = true
+        searchView.queryHint = getString(R.string.search_book_source)
+        searchView.clearFocus()
+        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+            override fun onQueryTextSubmit(query: String?): Boolean {
+                return false
+            }
+
+            override fun onQueryTextChange(newText: String?): Boolean {
+                initData(newText)
+                return false
+            }
+        })
+    }
+
+    private fun initData(searchKey: String? = null) {
+        sourceFlowJob?.cancel()
+        sourceFlowJob = launch {
+            when {
+                searchKey.isNullOrEmpty() -> appDb.bookSourceDao.flowEnabled()
+                else -> appDb.bookSourceDao.flowSearchEnabled(searchKey)
+            }.collect {
+                adapter.setItems(it)
+            }
+        }
+    }
+
+    inner class SourceAdapter(context: Context) :
+        RecyclerAdapter<BookSource, Item1lineTextBinding>(context) {
+
+        override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding {
+            return Item1lineTextBinding.inflate(inflater, parent, false).apply {
+                root.setPadding(16.dpToPx())
+            }
+        }
+
+        override fun convert(
+            holder: ItemViewHolder,
+            binding: Item1lineTextBinding,
+            item: BookSource,
+            payloads: MutableList<Any>
+        ) {
+            binding.textView.text = item.getDisPlayNameGroup()
+        }
+
+        override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) {
+            binding.root.onClick {
+                getItemByLayoutPosition(holder.layoutPosition)?.let {
+                    callback?.sourceOnClick(it)
+                    dismissAllowingStateLoss()
+                }
+            }
+        }
+
+    }
+
+    private val callback: Callback?
+        get() {
+            return (parentFragment as? Callback) ?: activity as? Callback
+        }
+
+    interface Callback {
+        fun sourceOnClick(source: BookSource)
+    }
+
+}

+ 21 - 2
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt

@@ -12,10 +12,12 @@ import androidx.activity.viewModels
 import androidx.compose.runtime.mutableStateOf
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
+import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.Status
 import io.legado.app.constant.Theme
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityAudioPlayBinding
 import io.legado.app.lib.dialogs.alert
@@ -24,6 +26,7 @@ import io.legado.app.model.BookCover
 import io.legado.app.service.AudioPlayService
 import io.legado.app.ui.about.AppLogDialog
 import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
+import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.toc.TocActivityResult
 import io.legado.app.ui.login.SourceLoginActivity
@@ -31,6 +34,9 @@ import io.legado.app.ui.theme.AppTheme
 import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import splitties.views.onLongClick
 import java.util.*
 
@@ -198,8 +204,21 @@ class AudioPlayActivity :
     override val oldBook: Book?
         get() = AudioPlay.book
 
-    override fun changeTo(source: BookSource, book: Book) {
-        viewModel.changeTo(source, book)
+    override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
+        if (book.type == BookType.audio) {
+            viewModel.changeTo(source, book, toc)
+        } else {
+            AudioPlay.stop(this)
+            launch {
+                withContext(IO) {
+                    AudioPlay.book?.changeTo(book, toc)
+                }
+                startActivity<ReadBookActivity> {
+                    putExtra("bookUrl", book.bookUrl)
+                }
+                finish()
+            }
+        }
     }
 
     override fun finish() {

+ 8 - 50
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt

@@ -9,8 +9,6 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
-import io.legado.app.help.BookHelp
-import io.legado.app.help.ContentProcessor
 import io.legado.app.model.AudioPlay
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.utils.postEvent
@@ -45,33 +43,23 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
         }
     }
 
-    private fun loadBookInfo(
-        book: Book,
-        changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
-    ) {
+    private fun loadBookInfo(book: Book) {
         execute {
             AudioPlay.bookSource?.let {
                 WebBook.getBookInfo(this, it, book)
                     .onSuccess {
-                        loadChapterList(book, changeDruChapterIndex)
+                        loadChapterList(book)
                     }
             }
         }
     }
 
-    private fun loadChapterList(
-        book: Book,
-        changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
-    ) {
+    private fun loadChapterList(book: Book) {
         execute {
             AudioPlay.bookSource?.let {
                 WebBook.getChapterList(this, it, book)
                     .onSuccess(Dispatchers.IO) { cList ->
-                        if (changeDruChapterIndex == null) {
-                            appDb.bookChapterDao.insert(*cList.toTypedArray())
-                        } else {
-                            changeDruChapterIndex(cList)
-                        }
+                        appDb.bookChapterDao.insert(*cList.toTypedArray())
                         AudioPlay.upDurChapter(book)
                     }.onError {
                         context.toastOnUi(R.string.error_load_toc)
@@ -88,47 +76,17 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
         }
     }
 
-    fun changeTo(source: BookSource, book: Book) {
+    fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
         execute {
-            var oldTocSize: Int = book.totalChapterNum
-            AudioPlay.book?.let {
-                oldTocSize = it.totalChapterNum
-                book.order = it.order
-                appDb.bookDao.delete(it)
-            }
-            appDb.bookDao.insert(book)
-            AudioPlay.book = book
+            AudioPlay.book = AudioPlay.book!!.changeTo(book, toc)
             AudioPlay.bookSource = source
-            if (book.tocUrl.isEmpty()) {
-                loadBookInfo(book) { upChangeDurChapterIndex(book, oldTocSize, it) }
-            } else {
-                loadChapterList(book) { upChangeDurChapterIndex(book, oldTocSize, it) }
-            }
+            appDb.bookChapterDao.insert(*toc.toTypedArray())
+            AudioPlay.upDurChapter(book)
         }.onFinally {
             postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
         }
     }
 
-    private fun upChangeDurChapterIndex(
-        book: Book,
-        oldTocSize: Int,
-        chapters: List<BookChapter>
-    ) {
-        execute {
-            book.durChapterIndex = BookHelp.getDurChapter(
-                book.durChapterIndex,
-                book.durChapterTitle,
-                chapters,
-                oldTocSize
-            )
-            book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
-                ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
-            )
-            appDb.bookDao.update(book)
-            appDb.bookChapterDao.insert(*chapters.toTypedArray())
-        }
-    }
-
     fun removeFromBookshelf(success: (() -> Unit)?) {
         execute {
             AudioPlay.book?.let {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/audio/ComposeView.kt

@@ -32,7 +32,7 @@ fun TimerDialog(state: MutableState<Boolean>, parent: View) {
                         timeMinute.value = it.toInt()
                         AudioPlay.setTimer(it.toInt())
                     },
-                    valueRange = 0f..180f
+                    valueRange = 0f..180f,
                 )
             }
         }

+ 47 - 0
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt

@@ -0,0 +1,47 @@
+package io.legado.app.ui.book.bookmark
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import io.legado.app.base.VMBaseActivity
+import io.legado.app.data.entities.Bookmark
+import io.legado.app.databinding.ActivityAllBookmarkBinding
+import io.legado.app.utils.showDialogFragment
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+
+class AllBookmarkActivity : VMBaseActivity<ActivityAllBookmarkBinding, AllBookmarkViewModel>(),
+    BookmarkAdapter.Callback,
+    BookmarkDialog.Callback {
+
+    override val viewModel by viewModels<AllBookmarkViewModel>()
+    override val binding by viewBinding(ActivityAllBookmarkBinding::inflate)
+    private val adapter by lazy {
+        BookmarkAdapter(this, this)
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        initView()
+        viewModel.initData {
+            adapter.setItems(it)
+        }
+    }
+
+    private fun initView() {
+        binding.recyclerView.addItemDecoration(BookmarkDecoration(adapter))
+        binding.recyclerView.adapter = adapter
+    }
+
+    override fun onItemClick(bookmark: Bookmark, position: Int) {
+        showDialogFragment(BookmarkDialog(bookmark, position))
+    }
+
+    override fun upBookmark(pos: Int, bookmark: Bookmark) {
+        adapter.setItem(pos, bookmark)
+    }
+
+    override fun deleteBookmark(pos: Int) {
+        adapter.getItem(pos)?.let {
+            viewModel.deleteBookmark(it)
+        }
+        adapter.removeItem(pos)
+    }
+}

+ 25 - 0
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt

@@ -0,0 +1,25 @@
+package io.legado.app.ui.book.bookmark
+
+import android.app.Application
+import io.legado.app.base.BaseViewModel
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.Bookmark
+
+class AllBookmarkViewModel(application: Application) : BaseViewModel(application) {
+
+
+    fun initData(onSuccess: (bookmarks: List<Bookmark>) -> Unit) {
+        execute {
+            appDb.bookmarkDao.all
+        }.onSuccess {
+            onSuccess.invoke(it)
+        }
+    }
+
+    fun deleteBookmark(bookmark: Bookmark) {
+        execute {
+            appDb.bookmarkDao.delete(bookmark)
+        }
+    }
+
+}

+ 60 - 0
app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt

@@ -0,0 +1,60 @@
+package io.legado.app.ui.book.bookmark
+
+import android.content.Context
+import android.view.ViewGroup
+import io.legado.app.base.adapter.ItemViewHolder
+import io.legado.app.base.adapter.RecyclerAdapter
+import io.legado.app.data.entities.Bookmark
+import io.legado.app.databinding.ItemBookmarkBinding
+import io.legado.app.utils.gone
+import splitties.views.onClick
+
+class BookmarkAdapter(context: Context, val callback: Callback) :
+    RecyclerAdapter<Bookmark, ItemBookmarkBinding>(context) {
+
+    override fun getViewBinding(parent: ViewGroup): ItemBookmarkBinding {
+        return ItemBookmarkBinding.inflate(inflater, parent, false)
+    }
+
+    override fun convert(
+        holder: ItemViewHolder,
+        binding: ItemBookmarkBinding,
+        item: Bookmark,
+        payloads: MutableList<Any>
+    ) {
+        binding.tvChapterName.text = item.chapterName
+        binding.tvBookText.gone(item.bookText.isEmpty())
+        binding.tvBookText.text = item.bookText
+        binding.tvContent.gone(item.content.isEmpty())
+        binding.tvContent.text = item.content
+    }
+
+    override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) {
+        binding.root.onClick {
+            getItemByLayoutPosition(holder.layoutPosition)?.let {
+                callback.onItemClick(it, holder.layoutPosition)
+            }
+        }
+    }
+
+    fun getHeaderText(position: Int): String {
+        return with(getItem(position)) {
+            "${this?.bookName ?: ""}(${this?.bookAuthor ?: ""})"
+        }
+    }
+
+    fun isItemHeader(position: Int): Boolean {
+        if (position == 0) return true
+        val lastItem = getItem(position - 1)
+        val curItem = getItem(position)
+        return !(lastItem?.bookName == curItem?.bookName
+            && lastItem?.bookAuthor == curItem?.bookAuthor)
+    }
+
+    interface Callback {
+
+        fun onItemClick(bookmark: Bookmark, position: Int)
+
+    }
+
+}

+ 110 - 0
app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt

@@ -0,0 +1,110 @@
+package io.legado.app.ui.book.bookmark
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.text.TextPaint
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.lib.theme.accentColor
+import io.legado.app.lib.theme.backgroundColor
+import io.legado.app.utils.dpToPx
+import io.legado.app.utils.spToPx
+import splitties.init.appCtx
+import kotlin.math.min
+
+class BookmarkDecoration(val adapter: BookmarkAdapter) : RecyclerView.ItemDecoration() {
+
+    private val headerLeft = 16f.dpToPx()
+    private val headerHeight = 32f.dpToPx()
+
+    private val headerPaint = Paint().apply {
+        color = appCtx.backgroundColor
+    }
+    private val textPaint = TextPaint().apply {
+        textSize = 16f.spToPx()
+        color = appCtx.accentColor
+    }
+    private val textRect = Rect()
+
+    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+        val count = parent.childCount
+        for (i in 0 until count) {
+            val view = parent.getChildAt(i)
+            val position = parent.getChildLayoutPosition(view)
+            val isHeader = adapter.isItemHeader(position)
+            if (isHeader) {
+                c.drawRect(
+                    0f,
+                    view.top - headerHeight,
+                    parent.width.toFloat(),
+                    view.top.toFloat(),
+                    headerPaint
+                )
+                val headerText = adapter.getHeaderText(position)
+                textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
+                c.drawText(
+                    headerText,
+                    headerLeft,
+                    (view.top - headerHeight) + headerHeight / 2 + textRect.height() / 2,
+                    textPaint
+                )
+            }
+        }
+    }
+
+    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+        val position = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
+        val view = parent.findViewHolderForAdapterPosition(position)?.itemView ?: return
+        val isHeader = adapter.isItemHeader(position + 1)
+        val headerText = adapter.getHeaderText(position)
+        if (isHeader) {
+            val bottom = min(headerHeight.toInt(), view.bottom)
+            c.drawRect(
+                0f,
+                view.top - headerHeight,
+                parent.width.toFloat(),
+                bottom.toFloat(),
+                headerPaint
+            )
+            textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
+            c.drawText(
+                headerText,
+                headerLeft,
+                headerHeight / 2 + textRect.height() / 2 - (headerHeight - bottom),
+                textPaint
+            )
+        } else {
+            c.drawRect(
+                0f,
+                0f,
+                parent.width.toFloat(),
+                headerHeight,
+                headerPaint
+            )
+            textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
+            c.drawText(
+                headerText,
+                headerLeft,
+                headerHeight / 2 + textRect.height() / 2,
+                textPaint
+            )
+        }
+        c.save()
+    }
+
+    override fun getItemOffsets(
+        outRect: Rect,
+        view: View,
+        parent: RecyclerView,
+        state: RecyclerView.State
+    ) {
+        val position = parent.getChildLayoutPosition(view)
+        val isHeader = adapter.isItemHeader(position)
+        if (isHeader) {
+            outRect.top = headerHeight.toInt()
+        }
+    }
+
+}

+ 4 - 3
app/src/main/java/io/legado/app/ui/book/toc/BookmarkDialog.kt → app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.book.toc
+package io.legado.app.ui.book.bookmark
 
 import android.os.Bundle
 import android.view.View
@@ -75,8 +75,9 @@ class BookmarkDialog() : BaseDialogFragment(R.layout.dialog_bookmark) {
         }
     }
 
-    fun getCallback(): Callback? {
-        return parentFragment as? Callback
+    private fun getCallback(): Callback? {
+        return (parentFragment as? Callback)
+            ?: activity as? Callback
     }
 
     interface Callback {

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt

@@ -74,7 +74,9 @@ class ChangeBookSourceAdapter(
     override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
         holder.itemView.setOnClickListener {
             getItem(holder.layoutPosition)?.let {
-                callBack.changeTo(it)
+                if (it.bookUrl != callBack.bookUrl) {
+                    callBack.changeTo(it)
+                }
             }
         }
         holder.itemView.onLongClick {

+ 40 - 21
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -19,6 +19,7 @@ import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.databinding.DialogBookChangeSourceBinding
@@ -27,6 +28,7 @@ import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
+import io.legado.app.ui.widget.dialog.WaitDialog
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -50,6 +52,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
     private val groups = linkedSetOf<String>()
     private val callBack: CallBack? get() = activity as? CallBack
     private val viewModel: ChangeBookSourceViewModel by viewModels()
+    private val waitDialog by lazy { WaitDialog(requireContext()) }
     private val adapter by lazy { ChangeBookSourceAdapter(requireContext(), viewModel, this) }
     private val editSourceResult =
         registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
@@ -57,14 +60,14 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
         }
     private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchGroup = getPrefString("searchGroup")
-            if (!searchGroup.isNullOrEmpty()) {
+            val searchGroup = AppConfig.searchGroup
+            if (searchGroup.isNotEmpty()) {
                 launch {
                     alert("搜索结果为空") {
                         setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
                         cancelButton()
                         okButton {
-                            putPrefString("searchGroup", "")
+                            AppConfig.searchGroup = ""
                             viewModel.startSearch()
                         }
                     }
@@ -218,9 +221,9 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
                 if (!item.isChecked) {
                     item.isChecked = true
                     if (item.title.toString() == getString(R.string.all_source)) {
-                        putPrefString("searchGroup", "")
+                        AppConfig.searchGroup = ""
                     } else {
-                        putPrefString("searchGroup", item.title.toString())
+                        AppConfig.searchGroup = item.title.toString()
                     }
                     viewModel.startOrStopSearch()
                     viewModel.refresh()
@@ -241,8 +244,23 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
     }
 
     override fun changeTo(searchBook: SearchBook) {
-        changeSource(searchBook)
-        dismissAllowingStateLoss()
+        if (searchBook.type == callBack?.oldBook?.type) {
+            changeSource(searchBook) {
+                dismissAllowingStateLoss()
+            }
+        } else {
+            alert(
+                titleResource = R.string.book_type_different,
+                messageResource = R.string.soure_change_source
+            ) {
+                okButton {
+                    changeSource(searchBook) {
+                        dismissAllowingStateLoss()
+                    }
+                }
+                cancelButton()
+            }
+        }
     }
 
     override val bookUrl: String?
@@ -269,22 +287,23 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
     override fun deleteSource(searchBook: SearchBook) {
         viewModel.del(searchBook)
         if (bookUrl == searchBook.bookUrl) {
-            viewModel.firstSourceOrNull(searchBook)?.let {
-                changeSource(it)
+            viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source ->
+                callBack?.changeTo(source, book, toc)
             }
         }
     }
 
-    private fun changeSource(searchBook: SearchBook) {
-        try {
-            val book = searchBook.toBook()
-            book.upInfoFromOld(callBack?.oldBook)
-            val source = appDb.bookSourceDao.getBookSource(book.origin)
-            callBack?.changeTo(source!!, book)
-            searchBook.time = System.currentTimeMillis()
-            viewModel.updateSource(searchBook)
-        } catch (e: Exception) {
-            toastOnUi("换源失败\n${e.localizedMessage}")
+    private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) {
+        waitDialog.setText(R.string.load_toc)
+        waitDialog.show()
+        val book = searchBook.toBook()
+        viewModel.getToc(book, {
+            waitDialog.dismiss()
+            toastOnUi(it)
+        }) { toc, source ->
+            waitDialog.dismiss()
+            callBack?.changeTo(source, book, toc)
+            onSuccess?.invoke()
         }
     }
 
@@ -293,7 +312,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
      */
     private fun upGroupMenu() {
         val menu: Menu = binding.toolBar.menu
-        val selectedGroup = getPrefString("searchGroup")
+        val selectedGroup = AppConfig.searchGroup
         menu.removeGroup(R.id.source_group)
         val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
         var hasSelectedGroup = false
@@ -325,7 +344,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
 
     interface CallBack {
         val oldBook: Book?
-        fun changeTo(source: BookSource, book: Book)
+        fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>)
     }
 
 }

+ 79 - 12
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt

@@ -11,14 +11,16 @@ import io.legado.app.constant.AppPattern
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.SearchBook
+import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.utils.getPrefBoolean
-import io.legado.app.utils.getPrefString
+import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.ExecutorCoroutineDispatcher
@@ -27,8 +29,9 @@ import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import splitties.init.appCtx
+import kotlinx.coroutines.withContext
 import java.util.*
+import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executors
 import kotlin.math.min
 
@@ -44,7 +47,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     private var screenKey: String = ""
     private var bookSourceList = arrayListOf<BookSource>()
     private val searchBooks = Collections.synchronizedList(arrayListOf<SearchBook>())
-    private val searchGroup get() = appCtx.getPrefString("searchGroup") ?: ""
+    private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
     private var searchCallback: SourceCallback? = null
     val searchDataFlow = callbackFlow {
 
@@ -86,6 +89,11 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     @Volatile
     private var searchIndex = -1
 
+    override fun onCleared() {
+        super.onCleared()
+        searchPool?.close()
+    }
+
     @CallSuper
     open fun initData(arguments: Bundle?) {
         arguments?.let { bundle ->
@@ -118,11 +126,13 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
             appDb.searchBookDao.clear(name, author)
             searchBooks.clear()
             bookSourceList.clear()
+            val searchGroup = AppConfig.searchGroup
             if (searchGroup.isBlank()) {
                 bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
             } else {
                 val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
                 if (sources.isEmpty()) {
+                    AppConfig.searchGroup = ""
                     bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
                 } else {
                     bookSourceList.addAll(sources)
@@ -187,6 +197,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
 
     private suspend fun loadBookToc(scope: CoroutineScope, source: BookSource, book: Book) {
         val chapters = WebBook.getChapterListAwait(scope, source, book)
+        tocMap[book.bookUrl] = chapters
         book.latestChapterTitle = chapters.last().title
         val searchBook: SearchBook = book.toSearchBook()
         searchCallback?.searchSuccess(searchBook)
@@ -212,15 +223,23 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     private fun getDbSearchBooks(): List<SearchBook> {
         return if (screenKey.isEmpty()) {
             if (AppConfig.changeSourceCheckAuthor) {
-                appDb.searchBookDao.getChangeSourceSearch(name, author, searchGroup)
+                appDb.searchBookDao.getChangeSourceSearch(
+                    name, author, AppConfig.searchGroup
+                )
             } else {
-                appDb.searchBookDao.getChangeSourceSearch(name, "", searchGroup)
+                appDb.searchBookDao.getChangeSourceSearch(
+                    name, "", AppConfig.searchGroup
+                )
             }
         } else {
             if (AppConfig.changeSourceCheckAuthor) {
-                appDb.searchBookDao.getChangeSourceSearch(name, author, screenKey, searchGroup)
+                appDb.searchBookDao.getChangeSourceSearch(
+                    name, author, screenKey, AppConfig.searchGroup
+                )
             } else {
-                appDb.searchBookDao.getChangeSourceSearch(name, "", screenKey, searchGroup)
+                appDb.searchBookDao.getChangeSourceSearch(
+                    name, "", screenKey, AppConfig.searchGroup
+                )
             }
         }
     }
@@ -253,9 +272,39 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         searchStateData.postValue(false)
     }
 
-    override fun onCleared() {
-        super.onCleared()
-        searchPool?.close()
+    fun getToc(
+        book: Book,
+        onError: (msg: String) -> Unit,
+        onSuccess: (toc: List<BookChapter>, source: BookSource) -> Unit
+    ) {
+        execute {
+            val toc = tocMap[book.bookUrl]
+            if (toc != null) {
+                val source = appDb.bookSourceDao.getBookSource(book.origin)
+                return@execute Pair(toc, source!!)
+            }
+            val result = getToc(book).getOrThrow()
+            tocMap[book.bookUrl] = result.first
+            return@execute result
+        }.onSuccess {
+            onSuccess.invoke(it.first, it.second)
+        }.onError {
+            onError.invoke(it.localizedMessage ?: "获取目录出错")
+        }
+    }
+
+    suspend fun getToc(book: Book): Result<Pair<List<BookChapter>, BookSource>> {
+        return kotlin.runCatching {
+            withContext(IO) {
+                val source = appDb.bookSourceDao.getBookSource(book.origin)
+                    ?: throw NoStackTraceException("书源不存在")
+                if (book.tocUrl.isEmpty()) {
+                    WebBook.getBookInfoAwait(this, source, book)
+                }
+                val toc = WebBook.getChapterListAwait(this, source, book)
+                Pair(toc, source)
+            }
+        }
     }
 
     fun disableSource(searchBook: SearchBook) {
@@ -310,8 +359,26 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         searchCallback?.upAdapter()
     }
 
-    fun firstSourceOrNull(searchBook: SearchBook): SearchBook? {
-        return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl }
+    fun autoChangeSource(
+        bookType: Int?,
+        onSuccess: (book: Book, toc: List<BookChapter>, source: BookSource) -> Unit
+    ) {
+        execute {
+            searchBooks.forEach {
+                if (it.type == bookType) {
+                    val book = it.toBook()
+                    val result = getToc(book).getOrNull()
+                    if (result != null) {
+                        return@execute Triple(book, result.first, result.second)
+                    }
+                }
+            }
+            throw NoStackTraceException("没有有效源")
+        }.onSuccess {
+            onSuccess.invoke(it.first, it.second, it.third)
+        }.onError {
+            context.toastOnUi("自动换源失败\n${it.localizedMessage}")
+        }
     }
 
     interface SourceCallback {

+ 17 - 30
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -67,13 +67,6 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
     private val tocAdapter by lazy {
         ChangeChapterTocAdapter(requireContext(), this)
     }
-    private val tocSuccess: (toc: List<BookChapter>) -> Unit = {
-        tocAdapter.durChapterIndex =
-            BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, it)
-        binding.loadingToc.hide()
-        tocAdapter.setItems(it)
-        binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5)
-    }
     private val contentSuccess: (content: String) -> Unit = {
         binding.loadingToc.hide()
         callBack?.replaceContent(it)
@@ -82,14 +75,14 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
     private var searchBook: SearchBook? = null
     private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchGroup = getPrefString("searchGroup")
-            if (!searchGroup.isNullOrEmpty()) {
+            val searchGroup = AppConfig.searchGroup
+            if (searchGroup.isNotEmpty()) {
                 launch {
                     alert("搜索结果为空") {
                         setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
                         cancelButton()
                         okButton {
-                            putPrefString("searchGroup", "")
+                            AppConfig.searchGroup = ""
                             viewModel.startSearch()
                         }
                     }
@@ -252,9 +245,9 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
                 if (!item.isChecked) {
                     item.isChecked = true
                     if (item.title.toString() == getString(R.string.all_source)) {
-                        putPrefString("searchGroup", "")
+                        AppConfig.searchGroup = ""
                     } else {
-                        putPrefString("searchGroup", item.title.toString())
+                        AppConfig.searchGroup = item.title.toString()
                     }
                     viewModel.startOrStopSearch()
                     viewModel.refresh()
@@ -279,9 +272,16 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
         tocAdapter.setItems(null)
         binding.clToc.visible()
         binding.loadingToc.show()
-        viewModel.getToc(searchBook, tocSuccess) {
+        val book = searchBook.toBook()
+        viewModel.getToc(book, {
             binding.clToc.gone()
             toastOnUi(it)
+        }) { toc: List<BookChapter>, _: BookSource ->
+            tocAdapter.durChapterIndex =
+                BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, toc)
+            binding.loadingToc.hide()
+            tocAdapter.setItems(toc)
+            binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5)
         }
     }
 
@@ -309,8 +309,8 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
     override fun deleteSource(searchBook: SearchBook) {
         viewModel.del(searchBook)
         if (bookUrl == searchBook.bookUrl) {
-            viewModel.firstSourceOrNull(searchBook)?.let {
-                changeSource(it)
+            viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source ->
+                callBack?.changeTo(source, book, toc)
             }
         }
     }
@@ -326,25 +326,12 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
         }
     }
 
-    private fun changeSource(searchBook: SearchBook) {
-        try {
-            val book = searchBook.toBook()
-            book.upInfoFromOld(callBack?.oldBook)
-            val source = appDb.bookSourceDao.getBookSource(book.origin)
-            callBack?.changeTo(source!!, book)
-            searchBook.time = System.currentTimeMillis()
-            viewModel.updateSource(searchBook)
-        } catch (e: Exception) {
-            toastOnUi("换源失败\n${e.localizedMessage}")
-        }
-    }
-
     /**
      * 更新分组菜单
      */
     private fun upGroupMenu() {
         val menu: Menu = binding.toolBar.menu
-        val selectedGroup = getPrefString("searchGroup")
+        val selectedGroup = AppConfig.searchGroup
         menu.removeGroup(R.id.source_group)
         val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
         var hasSelectedGroup = false
@@ -388,7 +375,7 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
 
     interface CallBack {
         val oldBook: Book?
-        fun changeTo(source: BookSource, book: Book)
+        fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>)
         fun replaceContent(content: String)
     }
 

+ 0 - 29
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt

@@ -5,10 +5,8 @@ import android.os.Bundle
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
-import io.legado.app.data.entities.SearchBook
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.model.webBook.WebBook
-import java.util.concurrent.ConcurrentHashMap
 
 @Suppress("MemberVisibilityCanBePrivate")
 class ChangeChapterSourceViewModel(application: Application) :
@@ -17,8 +15,6 @@ class ChangeChapterSourceViewModel(application: Application) :
     var chapterIndex: Int = 0
     var chapterTitle: String = ""
 
-    private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
-
     override fun initData(arguments: Bundle?) {
         super.initData(arguments)
         arguments?.let { bundle ->
@@ -29,31 +25,6 @@ class ChangeChapterSourceViewModel(application: Application) :
         }
     }
 
-    fun getToc(
-        searchBook: SearchBook,
-        success: (toc: List<BookChapter>) -> Unit,
-        error: (msg: String) -> Unit
-    ) {
-        execute {
-            return@execute tocMap[searchBook.bookUrl]
-                ?: let {
-                    val book = searchBook.toBook()
-                    val source = appDb.bookSourceDao.getBookSource(book.origin)
-                        ?: throw NoStackTraceException("书源不存在")
-                    if (book.tocUrl.isEmpty()) {
-                        WebBook.getBookInfoAwait(this, source, book)
-                    }
-                    val toc = WebBook.getChapterListAwait(this, source, book)
-                    tocMap[book.bookUrl] = toc
-                    toc
-                }
-        }.onSuccess {
-            success(it)
-        }.onError {
-            error(it.localizedMessage ?: "获取目录出错")
-        }
-    }
-
     fun getContent(
         book: Book,
         chapter: BookChapter,

+ 2 - 3
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -457,9 +457,8 @@ class BookInfoActivity :
     override val oldBook: Book?
         get() = viewModel.bookData.value
 
-    override fun changeTo(source: BookSource, book: Book) {
-        upLoading(true)
-        viewModel.changeTo(source, book)
+    override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
+        viewModel.changeTo(source, book, toc)
     }
 
     override fun coverChangeTo(coverUrl: String) {

+ 10 - 58
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -14,7 +14,6 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.BookHelp
-import io.legado.app.help.ContentProcessor
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.BookCover
 import io.legado.app.model.ReadBook
@@ -24,7 +23,6 @@ import io.legado.app.utils.postEvent
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.ensureActive
 
 class BookInfoViewModel(application: Application) : BaseViewModel(application) {
     val bookData = MutableLiveData<Book>()
@@ -107,12 +105,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
     fun loadBookInfo(
         book: Book,
         canReName: Boolean = true,
-        scope: CoroutineScope = viewModelScope,
-        changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
+        scope: CoroutineScope = viewModelScope
     ) {
         execute(scope) {
             if (book.isLocalBook()) {
-                loadChapter(book, scope, changeDruChapterIndex)
+                loadChapter(book, scope)
             } else {
                 bookSource?.let { bookSource ->
                     WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
@@ -121,7 +118,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
                             if (inBookshelf) {
                                 appDb.bookDao.update(book)
                             }
-                            loadChapter(it, scope, changeDruChapterIndex)
+                            loadChapter(it, scope)
                         }.onError {
                             AppLog.put("获取数据信息失败\n${it.localizedMessage}", it)
                             context.toastOnUi(R.string.error_get_book_info)
@@ -136,8 +133,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
 
     private fun loadChapter(
         book: Book,
-        scope: CoroutineScope = viewModelScope,
-        changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
+        scope: CoroutineScope = viewModelScope
     ) {
         execute(scope) {
             if (book.isLocalBook()) {
@@ -156,11 +152,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
                                 appDb.bookChapterDao.delByBook(book.bookUrl)
                                 appDb.bookChapterDao.insert(*it.toTypedArray())
                             }
-                            if (changeDruChapterIndex == null) {
-                                chapterListData.postValue(it)
-                            } else {
-                                changeDruChapterIndex(it)
-                            }
+                            chapterListData.postValue(it)
                         }.onError {
                             chapterListData.postValue(emptyList())
                             AppLog.put("获取目录失败\n${it.localizedMessage}", it)
@@ -184,58 +176,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-    fun changeTo(source: BookSource, newBook: Book) {
+    fun changeTo(source: BookSource, newBook: Book, toc: List<BookChapter>) {
         changeSourceCoroutine?.cancel()
         changeSourceCoroutine = execute {
-            var oldTocSize: Int = newBook.totalChapterNum
-            if (inBookshelf) {
-                bookData.value?.let {
-                    oldTocSize = it.totalChapterNum
-                    it.changeTo(newBook)
-                }
-            }
-            bookData.postValue(newBook)
             bookSource = source
-            if (newBook.tocUrl.isEmpty()) {
-                loadBookInfo(newBook, false, this) {
-                    ensureActive()
-                    upChangeDurChapterIndex(newBook, oldTocSize, it)
-                }
-            } else {
-                loadChapter(newBook, this) {
-                    ensureActive()
-                    upChangeDurChapterIndex(newBook, oldTocSize, it)
-                }
-            }
+            bookData.value!!.changeTo(newBook, toc)
+            bookData.postValue(newBook)
+            chapterListData.postValue(toc)
         }.onFinally {
             postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
         }
     }
 
-    private fun upChangeDurChapterIndex(
-        book: Book,
-        oldTocSize: Int,
-        chapters: List<BookChapter>
-    ) {
-        execute {
-            book.durChapterIndex = BookHelp.getDurChapter(
-                book.durChapterIndex,
-                book.durChapterTitle,
-                chapters,
-                oldTocSize
-            )
-            book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
-                ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
-            )
-            if (inBookshelf) {
-                appDb.bookDao.update(book)
-                appDb.bookChapterDao.insert(*chapters.toTypedArray())
-            }
-            bookData.postValue(book)
-            chapterListData.postValue(chapters)
-        }
-    }
-
     fun topBook() {
         execute {
             bookData.value?.let { book ->
@@ -300,7 +252,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
     fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
         execute {
             bookData.value?.let {
-                Book.delete(it)
+                it.delete()
                 inBookshelf = false
                 if (it.isLocalBook()) {
                     LocalBook.deleteBook(it, deleteOriginal)

+ 16 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -14,6 +14,7 @@ import androidx.core.view.size
 import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
 import io.legado.app.BuildConfig
 import io.legado.app.R
+import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.Status
@@ -38,6 +39,8 @@ import io.legado.app.model.ReadBook
 import io.legado.app.receiver.TimeBatteryReceiver
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.about.AppLogDialog
+import io.legado.app.ui.book.audio.AudioPlayActivity
+import io.legado.app.ui.book.bookmark.BookmarkDialog
 import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
 import io.legado.app.ui.book.changesource.ChangeChapterSourceDialog
 import io.legado.app.ui.book.read.config.*
@@ -51,7 +54,6 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.ui.book.searchContent.SearchContentActivity
 import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
-import io.legado.app.ui.book.toc.BookmarkDialog
 import io.legado.app.ui.book.toc.TocActivityResult
 import io.legado.app.ui.browser.WebViewActivity
 import io.legado.app.ui.dict.DictDialog
@@ -715,8 +717,19 @@ class ReadBookActivity : BaseReadBookActivity(),
     override val oldBook: Book?
         get() = ReadBook.book
 
-    override fun changeTo(source: BookSource, book: Book) {
-        viewModel.changeTo(source, book)
+    override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
+        if (book.type != BookType.audio) {
+            viewModel.changeTo(source, book, toc)
+        } else {
+            ReadAloud.stop(this)
+            launch {
+                ReadBook.book?.changeTo(book, toc)
+            }
+            startActivity<AudioPlayActivity> {
+                putExtra("bookUrl", book.bookUrl)
+            }
+            finish()
+        }
     }
 
     override fun replaceContent(content: String) {

+ 19 - 30
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -10,6 +10,7 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.EventBus
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
@@ -30,7 +31,6 @@ import io.legado.app.utils.postEvent
 import io.legado.app.utils.toStringArray
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.ensureActive
 
 class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     val permissionDenialLiveData = MutableLiveData<Int>()
@@ -191,40 +191,22 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     /**
      * 换源
      */
-    fun changeTo(source: BookSource, book: Book) {
+    fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
         changeSourceCoroutine?.cancel()
         changeSourceCoroutine = execute {
             ReadBook.upMsg(context.getString(R.string.loading))
-            if (book.tocUrl.isEmpty()) {
-                WebBook.getBookInfoAwait(this, source, book)
-            }
-            ensureActive()
-            val chapters = WebBook.getChapterListAwait(this, source, book)
-            ensureActive()
-            val oldBook = ReadBook.book!!
-            book.durChapterIndex = BookHelp.getDurChapter(
-                oldBook.durChapterIndex,
-                oldBook.durChapterTitle,
-                chapters,
-                oldBook.totalChapterNum
-            )
-            book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
-                ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
-            )
-            ensureActive()
-            val nextChapter = chapters.getOrElse(book.durChapterIndex) {
-                chapters.first()
+            ReadBook.book!!.changeTo(book, toc)
+            val nextChapter = toc.getOrElse(book.durChapterIndex) {
+                toc.first()
             }
             WebBook.getContentAwait(
                 this,
                 bookSource = source,
                 book = book,
-                bookChapter = chapters[book.durChapterIndex],
+                bookChapter = toc[book.durChapterIndex],
                 nextChapterUrl = nextChapter.url
             )
-            ensureActive()
-            oldBook.changeTo(book)
-            appDb.bookChapterDao.insert(*chapters.toTypedArray())
+            appDb.bookChapterDao.insert(*toc.toTypedArray())
             ReadBook.resetData(book)
             ReadBook.upMsg(null)
             ReadBook.loadContent(resetPageOffset = true)
@@ -244,10 +226,17 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
         if (!AppConfig.autoChangeSource) return
         execute {
             val sources = appDb.bookSourceDao.allTextEnabled
-            WebBook.preciseSearchAwait(this, sources, name, author)?.let {
-                it.second.upInfoFromOld(ReadBook.book)
-                changeTo(it.first, it.second)
-            } ?: throw NoStackTraceException("自动换源失败")
+            sources.forEach { source ->
+                WebBook.preciseSearchAwait(this, source, name, author).getOrNull()?.let { book ->
+                    if (book.tocUrl.isEmpty()) {
+                        WebBook.getBookInfoAwait(this, source, book)
+                    }
+                    val toc = WebBook.getChapterListAwait(this, source, book)
+                    changeTo(source, book, toc)
+                    return@execute
+                }
+            }
+            throw NoStackTraceException("自动换源失败")
         }.onStart {
             ReadBook.upMsg(context.getString(R.string.source_auto_changing))
         }.onError {
@@ -272,7 +261,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
 
     fun removeFromBookshelf(success: (() -> Unit)?) {
         execute {
-            Book.delete(ReadBook.book)
+            ReadBook.book?.delete()
         }.onSuccess {
             success?.invoke()
         }

+ 7 - 6
app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt

@@ -22,6 +22,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.SearchKeyword
 import io.legado.app.databinding.ActivityBookSearchBinding
+import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.*
 import io.legado.app.ui.book.info.BookInfoActivity
@@ -66,14 +67,14 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
     private var groups = linkedSetOf<String>()
     private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchGroup = getPrefString("searchGroup")
-            if (!searchGroup.isNullOrEmpty()) {
+            val searchGroup = AppConfig.searchGroup
+            if (searchGroup.isNotEmpty()) {
                 launch {
                     alert("搜索结果为空") {
                         setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
                         cancelButton()
                         okButton {
-                            putPrefString("searchGroup", "")
+                            AppConfig.searchGroup = ""
                             viewModel.searchKey = ""
                             viewModel.search(searchView.query.toString())
                         }
@@ -123,9 +124,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
             else -> if (item.groupId == R.id.source_group) {
                 item.isChecked = true
                 if (item.title.toString() == getString(R.string.all_source)) {
-                    putPrefString("searchGroup", "")
+                    AppConfig.searchGroup = ""
                 } else {
-                    putPrefString("searchGroup", item.title.toString())
+                    AppConfig.searchGroup = item.title.toString()
                 }
                 searchView.query?.toString()?.trim()?.let {
                     searchView.setQuery(it, true)
@@ -280,7 +281,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
      * 更新分组菜单
      */
     private fun upGroupMenu() = menu?.let { menu ->
-        val selectedGroup = getPrefString("searchGroup")
+        val selectedGroup = AppConfig.searchGroup
         menu.removeGroup(R.id.source_group)
         val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
         var hasSelectedGroup = false

+ 3 - 5
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt

@@ -86,14 +86,12 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
             text.isJsonArray() -> {
                 val items: List<Map<String, Any>> = jsonPath.parse(text).read("$")
                 val jsonItem = jsonPath.parse(items[0])
-                BookSource.fromJson(jsonItem.jsonString())
+                BookSource.fromJson(jsonItem.jsonString()).getOrThrow()
             }
             text.isJsonObject() -> {
-                BookSource.fromJson(text)
-            }
-            else -> {
-                null
+                BookSource.fromJson(text).getOrThrow()
             }
+            else -> throw NoStackTraceException("格式不对")
         }
     }
 

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -230,10 +230,10 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 }
                 searchKey.startsWith("group:") -> {
                     val key = searchKey.substringAfter("group:")
-                    appDb.bookSourceDao.flowGroupSearch("%$key%")
+                    appDb.bookSourceDao.flowGroupSearch(key)
                 }
                 else -> {
-                    appDb.bookSourceDao.flowSearch("%$searchKey%")
+                    appDb.bookSourceDao.flowSearch(searchKey)
                 }
             }.conflate().map { data ->
                 if (sortAscending) when (sort) {

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt

@@ -6,6 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.databinding.ItemBookmarkBinding
+import io.legado.app.utils.gone
 import splitties.views.onLongClick
 
 class BookmarkAdapter(context: Context, val callback: Callback) :
@@ -22,7 +23,9 @@ class BookmarkAdapter(context: Context, val callback: Callback) :
         payloads: MutableList<Any>
     ) {
         binding.tvChapterName.text = item.chapterName
+        binding.tvBookText.gone(item.bookText.isEmpty())
         binding.tvBookText.text = item.bookText
+        binding.tvContent.gone(item.content.isEmpty())
         binding.tvContent.text = item.content
     }
 

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt

@@ -11,6 +11,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.databinding.FragmentBookmarkBinding
 import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.book.bookmark.BookmarkDialog
 import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.utils.setEdgeEffectColor

+ 4 - 1
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -22,6 +22,7 @@ import io.legado.app.databinding.ActivityMainBinding
 import io.legado.app.help.BookHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
+import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.storage.Backup
 import io.legado.app.lib.theme.elevation
 import io.legado.app.lib.theme.primaryColor
@@ -175,7 +176,9 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
 
     override fun onDestroy() {
         super.onDestroy()
-        BookHelp.clearRemovedCache()
+        Coroutine.async {
+            BookHelp.clearInvalidCache()
+        }
     }
 
     override fun observeLiveBus() {

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -132,7 +132,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
                 if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
                     WebBook.preciseSearch(this, bookSources, name, author)
                         .onSuccess {
-                            val book = it.second
+                            val book = it.first
                             if (groupId > 0) {
                                 book.group = groupId
                             }

+ 2 - 2
app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt

@@ -123,10 +123,10 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
                 }
                 searchKey.startsWith("group:") -> {
                     val key = searchKey.substringAfter("group:")
-                    appDb.bookSourceDao.flowGroupExplore("%$key%")
+                    appDb.bookSourceDao.flowGroupExplore(key)
                 }
                 else -> {
-                    appDb.bookSourceDao.flowExplore("%$searchKey%")
+                    appDb.bookSourceDao.flowExplore(searchKey)
                 }
             }.catch {
                 AppLog.put("发现界面更新数据出错", it)

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt

@@ -20,6 +20,7 @@ import io.legado.app.service.WebService
 import io.legado.app.ui.about.AboutActivity
 import io.legado.app.ui.about.DonateActivity
 import io.legado.app.ui.about.ReadRecordActivity
+import io.legado.app.ui.book.bookmark.AllBookmarkActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.config.ConfigActivity
 import io.legado.app.ui.config.ConfigTag
@@ -135,6 +136,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
             when (preference.key) {
                 "bookSourceManage" -> startActivity<BookSourceActivity>()
                 "replaceManage" -> startActivity<ReplaceRuleActivity>()
+                "bookmark" -> startActivity<AllBookmarkActivity>()
                 "setting" -> startActivity<ConfigActivity> {
                     putExtra("configTag", ConfigTag.OTHER_CONFIG)
                 }

+ 2 - 2
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt

@@ -52,7 +52,7 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
         execute(context = Dispatchers.Main) {
             var source: RssSource? = null
             context.getClipText()?.let { json ->
-                source = RssSource.fromJson(json)
+                source = RssSource.fromJson(json).getOrThrow()
             }
             source
         }.onError {
@@ -69,7 +69,7 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
     fun importSource(text: String, finally: (source: RssSource) -> Unit) {
         execute {
             val text1 = text.trim()
-            RssSource.fromJson(text1)?.let {
+            RssSource.fromJson(text1).getOrThrow().let {
                 finally.invoke(it)
             }
         }.onError {

+ 23 - 10
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -6,23 +6,36 @@ import androidx.compose.material.lightColors
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import io.legado.app.help.config.ThemeConfig
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.primaryColor
+import io.legado.app.utils.ColorUtils
 import splitties.init.appCtx
 
+object AppTheme {
+
+    val colors
+        get() = if (ThemeConfig.isDarkTheme()) {
+            darkColors(
+                primary = Color(appCtx.accentColor),
+                primaryVariant = Color(ColorUtils.darkenColor(appCtx.accentColor)),
+                secondary = Color(appCtx.primaryColor),
+                secondaryVariant = Color(appCtx.primaryColor)
+            )
+        } else {
+            lightColors(
+                primary = Color(appCtx.accentColor),
+                primaryVariant = Color(ColorUtils.darkenColor(appCtx.accentColor)),
+                secondary = Color(appCtx.primaryColor),
+                secondaryVariant = Color(appCtx.primaryColor)
+            )
+        }
+
+}
 
 @Composable
 fun AppTheme(content: @Composable () -> Unit) {
-    val colors = if (ThemeConfig.isDarkTheme()) {
-        darkColors(
-            primary = Color(appCtx.primaryColor),
-        )
-    } else {
-        lightColors(
-            primary = Color(appCtx.primaryColor),
-        )
-    }
     MaterialTheme(
-        colors = colors,
+        colors = AppTheme.colors,
         content = content
     )
 }

+ 5 - 0
app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt

@@ -98,6 +98,11 @@ class SelectActionBar @JvmOverloads constructor(
         btnRevertSelection.isClickable = isClickable
         btnSelectActionMain.isEnabled = isClickable
         btnSelectActionMain.isClickable = isClickable
+        if (isClickable) {
+            ivMenuMore.setColorFilter(context.primaryTextColor)
+        } else {
+            ivMenuMore.setColorFilter(context.secondaryTextColor)
+        }
         ivMenuMore.isEnabled = isClickable
         ivMenuMore.isClickable = isClickable
     }

+ 16 - 0
app/src/main/java/io/legado/app/utils/GsonExtensions.kt

@@ -4,6 +4,8 @@ import com.google.gson.*
 import com.google.gson.internal.LinkedTreeMap
 import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonWriter
+import java.io.InputStream
+import java.io.InputStreamReader
 import java.io.OutputStream
 import java.io.OutputStreamWriter
 import java.lang.reflect.ParameterizedType
@@ -37,6 +39,20 @@ inline fun <reified T> Gson.fromJsonArray(json: String?): Result<List<T>?> {
     }
 }
 
+inline fun <reified T> Gson.fromJsonObject(inputStream: InputStream?): Result<T?> {
+    return kotlin.runCatching {
+        val reader = InputStreamReader(inputStream)
+        fromJson(reader, genericType<T>()) as? T
+    }
+}
+
+inline fun <reified T> Gson.fromJsonArray(inputStream: InputStream?): Result<List<T>?> {
+    return kotlin.runCatching {
+        val reader = InputStreamReader(inputStream)
+        fromJson(reader, ParameterizedTypeImpl(T::class.java)) as? List<T>
+    }
+}
+
 fun Gson.writeToOutputStream(out: OutputStream, any: Any) {
     val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
     writer.setIndent("  ")

+ 8 - 0
app/src/main/java/io/legado/app/utils/ViewExtensions.kt

@@ -98,6 +98,14 @@ fun View.gone() {
     }
 }
 
+fun View.gone(gone: Boolean) {
+    if (gone) {
+        gone()
+    } else {
+        visibility = VISIBLE
+    }
+}
+
 fun View.invisible() {
     if (visibility != INVISIBLE) {
         visibility = INVISIBLE

+ 22 - 0
app/src/main/res/layout/activity_all_bookmark.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <io.legado.app.ui.widget.TitleBar
+        android:id="@+id/title_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:title="@string/all_bookmark"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintTop_toBottomOf="@+id/title_bar"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 14 - 5
app/src/main/res/layout/activity_arrange_book.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:orientation="vertical"
     android:layout_width="match_parent"
@@ -9,17 +9,26 @@
         android:id="@+id/title_bar"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        app:title="@string/arrange_bookshelf" />
+        app:title="@string/arrange_bookshelf"
+        app:layout_constraintTop_toTopOf="parent" />
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/recycler_view"
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        android:layout_weight="1" />
+        app:layout_constraintTop_toBottomOf="@id/title_bar"
+        app:layout_constraintBottom_toTopOf="@id/select_action_bar" />
 
     <io.legado.app.ui.widget.SelectActionBar
         android:id="@+id/select_action_bar"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <androidx.compose.ui.platform.ComposeView
+        android:id="@+id/compose_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent" />
 
-</LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 0
app/src/main/res/layout/activity_main.xml

@@ -18,6 +18,7 @@
         android:layout_width="match_parent"
         android:layout_height="50dp"
         android:background="@color/background"
+        app:itemActiveIndicatorStyle="@color/transparent"
         app:labelVisibilityMode="unlabeled"
         app:menu="@menu/main_bnv" />
 

+ 100 - 0
app/src/main/res/layout/dialog_source_picker.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <io.legado.app.ui.widget.TitleBar
+        android:id="@+id/tool_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="?attr/actionBarStyle"
+        android:paddingRight="16dp"
+        app:attachToActivity="false"
+        app:contentLayout="@layout/view_search"
+        app:fitStatusBar="false"
+        app:popupTheme="@style/AppTheme.PopupOverlay"
+        app:titleTextAppearance="@style/ToolbarTitle"
+        tools:ignore="RtlHardcoded,RtlSymmetry" />
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
+            android:id="@+id/recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scrollbars="none"
+            tools:ignore="SpeakableTextPresentCheck" />
+
+        <io.legado.app.ui.widget.anima.RotateLoading
+            android:id="@+id/rotate_loading"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_gravity="center"
+            android:layout_margin="6dp"
+            android:visibility="gone"
+            app:loading_width="2dp" />
+
+        <TextView
+            android:id="@+id/tv_msg"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:padding="16dp"
+            android:textColor="@color/secondaryText"
+            android:visibility="gone" />
+
+    </FrameLayout>
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        app:flexWrap="wrap"
+        app:justifyContent="space_between">
+
+        <io.legado.app.ui.widget.text.AccentTextView
+            android:id="@+id/tv_footer_left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="12dp"
+            android:visibility="gone"
+            tools:ignore="RtlHardcoded" />
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="0dp" />
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <io.legado.app.ui.widget.text.AccentTextView
+                android:id="@+id/tv_cancel"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="12dp"
+                android:text="@string/cancel"
+                android:visibility="gone"
+                tools:ignore="RtlHardcoded" />
+
+            <io.legado.app.ui.widget.text.AccentTextView
+                android:id="@+id/tv_ok"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="12dp"
+                android:text="@string/ok"
+                android:visibility="gone"
+                tools:ignore="RtlHardcoded" />
+
+        </LinearLayout>
+
+    </com.google.android.flexbox.FlexboxLayout>
+</LinearLayout>

+ 1 - 0
app/src/main/res/layout/dialog_text_view.xml

@@ -10,6 +10,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:padding="12dp"
+        android:textColor="@color/secondaryText"
         android:textIsSelectable="true" />
 
     <io.legado.app.ui.widget.text.BadgeView

+ 20 - 0
app/src/main/res/layout/item_1line_text.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:padding="6dp"
+    android:background="?attr/selectableItemBackground"
+    tools:ignore="UseCompoundDrawables">
+
+    <TextView
+        android:id="@+id/text_view"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:singleLine="true"
+        android:textColor="@color/primaryText" />
+
+</LinearLayout>

+ 1 - 0
app/src/main/res/layout/item_1line_text_and_del.xml

@@ -14,6 +14,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
+        android:background="?attr/selectableItemBackground"
         android:singleLine="true"
         android:textColor="@color/primaryText" />
 

+ 1 - 0
app/src/main/res/layout/view_tab_layout.xml

@@ -4,6 +4,7 @@
     android:id="@+id/tab_layout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="@android:color/transparent"
     app:tabBackground="@android:color/transparent"
     app:tabRippleColor="@android:color/transparent"
     android:theme="?attr/actionBarStyle" />

+ 1 - 0
app/src/main/res/layout/view_tab_layout_min.xml

@@ -5,6 +5,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:theme="?attr/actionBarStyle"
+    android:background="@android:color/transparent"
     app:tabBackground="@android:color/transparent"
     app:tabRippleColor="@android:color/transparent"
     app:tabMaxWidth="100dp"

+ 4 - 0
app/src/main/res/menu/arrange_book_sel.xml

@@ -17,4 +17,8 @@
         android:id="@+id/menu_add_to_group"
         android:title="@string/add_to_group" />
 
+    <item
+        android:id="@+id/menu_change_source"
+        android:title="@string/change_source_batch" />
+
 </menu>

+ 6 - 2
app/src/main/res/values-es-rES/strings.xml

@@ -776,8 +776,8 @@
     <string name="select_theme">Cambiar tema predeterminado</string>
     <string name="sort_by_lastUpdateTime">Ordenar por hora de actualización</string>
     <string name="search_content">Contenido de búsqueda</string>
-    <string name="rss_source_empty">¡Siga [开源阅读] en WeChat para obtener más feeds RSS!</string>
-    <string name="explore_empty">Actualmente no hay una fuente para Descubrir, siga el número público [开源 阅读] para agregar una fuente de libro a través de Descubrir.</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">Actualmente no hay una fuente para Descubrir!</string>
     <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
     <string name="theme_name">Nombre del tema</string>
     <string name="auto_clear_expired">Limpiar automáticamente los datos de búsqueda caducados</string>
@@ -965,5 +965,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values-ja-rJP/strings.xml

@@ -46,7 +46,7 @@
     <string name="recent_reading">Recent reading</string>
     <string name="last_read">Last reading</string>
     <string name="update_log">What\'s new</string>
-    <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please pay attention to the public account [开源阅读] to get the book source!</string>
+    <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please open legado.top get help!</string>
     <string name="action_search">Search</string>
     <string name="action_download">Download</string>
     <string name="layout_list">List</string>
@@ -779,8 +779,8 @@
     <string name="select_theme">Switch default theme</string>
     <string name="sort_by_lastUpdateTime">Sort by update time</string>
     <string name="search_content">Search content</string>
-    <string name="rss_source_empty">Follow Wechat official account [开源阅读] to get Subscription source</string>
-    <string name="explore_empty">Empty now,Follow Wechat official account [开源阅读] to get Discovery source</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">Empty now!</string>
     <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
     <string name="theme_name">Theme name</string>
     <string name="auto_clear_expired">"Clear expired search histories automatically "</string>
@@ -968,5 +968,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values-pt-rBR/strings.xml

@@ -46,7 +46,7 @@
     <string name="recent_reading">Leitura recente</string>
     <string name="last_read">Última leitura</string>
     <string name="update_log">Novidades</string>
-    <string name="bookshelf_empty">A estante está vazia. Procure por livros ou adicione-os via descoberta! \n se você o usa pela primeira vez, por favor, preste atenção à conta pública [开源阅读] para obter a fonte do livro!</string>
+    <string name="bookshelf_empty">A estante está vazia. Procure por livros ou adicione-os via descoberta!</string>
     <string name="action_search">Pesquisar</string>
     <string name="action_download">Download</string>
     <string name="layout_list">Lista</string>
@@ -777,8 +777,8 @@
     <string name="select_theme">Alterar o tema padrão</string>
     <string name="sort_by_lastUpdateTime">Ordenar por tempo de atualização</string>
     <string name="search_content">Pesquisar conteúdo</string>
-    <string name="rss_source_empty">Siga [开源阅读] no WeChat para mais fontes de RSS!</string>
-    <string name="explore_empty">Atualmente não há nenhuma fonte, siga o número público [开源阅读] para adicionar uma fonte de livro via Descoberta</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">Atualmente não há nenhuma fonte!</string>
     <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
     <string name="theme_name">Nome de tema</string>
     <string name="auto_clear_expired">Excluir automaticamente dados vencidos da busca</string>
@@ -968,5 +968,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values-zh-rHK/strings.xml

@@ -43,7 +43,7 @@
     <string name="recent_reading">最近閲讀</string>
     <string name="last_read">最後閲讀</string>
     <string name="update_log">更新日誌</string>
-    <string name="bookshelf_empty">書架還空著,先去搜索書籍或從發現裏添加吧!\n如果初次使用請先關註公眾號[开源阅读]獲取書源!</string>
+    <string name="bookshelf_empty">書架還空著,先去搜索書籍或從發現裏添加吧!</string>
     <string name="action_search">搜尋</string>
     <string name="action_download">下載</string>
     <string name="layout_list">列表</string>
@@ -775,8 +775,8 @@
     <string name="share_selected_source">分享選中源</string>
     <string name="sort_by_lastUpdateTime">更新時間排序</string>
     <string name="search_content">全文搜索</string>
-    <string name="rss_source_empty">關注公眾號[开源阅读]獲取訂閲源!</string>
-    <string name="explore_empty">目前沒有發現源,關注公眾號[开源阅读]添加包含發現的書源!</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">目前沒有發現源!</string>
     <string name="page_key_set_help">將焦點放到輸入框按下物理按鍵會自動輸入鍵值,多個按鍵會自動用英文逗號隔開.</string>
     <string name="theme_name">主題名</string>
     <string name="auto_clear_expired">自動清除過期搜尋資料</string>
@@ -965,5 +965,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values-zh-rTW/strings.xml

@@ -45,7 +45,7 @@
     <string name="recent_reading">最近閱讀</string>
     <string name="last_read">最後閱讀</string>
     <string name="update_log">更新日誌</string>
-    <string name="bookshelf_empty">書架還空著,先去搜尋書籍或從發現裡添加吧!\n如果初次使用請先關注公眾號[开源阅读]獲取書源!</string>
+    <string name="bookshelf_empty">書架還空著,先去搜尋書籍或從發現裡添加吧!</string>
     <string name="action_search">搜尋</string>
     <string name="action_download">下載</string>
     <string name="layout_list">列表</string>
@@ -778,8 +778,8 @@
     <string name="select_theme">切換預設主題</string>
     <string name="sort_by_lastUpdateTime">更新時間排序</string>
     <string name="search_content">全文搜尋</string>
-    <string name="rss_source_empty">關注公眾號[开源阅读]獲取訂閱源!</string>
-    <string name="explore_empty">目前沒有發現源,關注公眾號[开源阅读]添加包含發現的書源!</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">目前沒有發現源!</string>
     <string name="page_key_set_help">將焦點放到輸入框按下物理按鍵會自動輸入鍵值,多個按鍵會自動用英文逗號隔開.</string>
     <string name="theme_name">主題名稱</string>
     <string name="auto_clear_expired">自動清除過期搜尋資料</string>
@@ -967,5 +967,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values-zh/strings.xml

@@ -45,7 +45,7 @@
     <string name="recent_reading">最近阅读</string>
     <string name="last_read">最后阅读</string>
     <string name="update_log">更新日志</string>
-    <string name="bookshelf_empty">书架还空着,先去搜索书籍或从发现里添加吧!\n初次使用请关注公众号[开源阅读]获取书源和教程!</string>
+    <string name="bookshelf_empty">书架还空着,先去搜索书籍或从发现里添加吧!</string>
     <string name="action_search">搜索</string>
     <string name="action_download">下载</string>
     <string name="layout_list">列表</string>
@@ -780,8 +780,8 @@
     <string name="search_content">全文搜索</string>
     <string name="search_content_size">搜索结果</string>
     <string name="search_content_empty">搜索结果为空</string>
-    <string name="rss_source_empty">关注公众号[开源阅读]获取订阅源!</string>
-    <string name="explore_empty">当前没有发现源,关注公众号[开源阅读]添加带发现的书源!</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">当前没有发现源!</string>
     <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
     <string name="theme_name">主题名称</string>
     <string name="auto_clear_expired">自动清除过期搜索数据</string>
@@ -967,5 +967,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 3
app/src/main/res/values/strings.xml

@@ -46,7 +46,7 @@
     <string name="recent_reading">Recent reading</string>
     <string name="last_read">Last reading</string>
     <string name="update_log">What\'s new</string>
-    <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please pay attention to the public account [开源阅读] to get the book source!</string>
+    <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery!</string>
     <string name="action_search">Search</string>
     <string name="action_download">Download</string>
     <string name="layout_list">List</string>
@@ -780,8 +780,8 @@
     <string name="sort_by_lastUpdateTime">Sort by update time</string>
     <string name="sort_by_respondTime">Sort by respond time</string>
     <string name="search_content">Search content</string>
-    <string name="rss_source_empty">Follow Wechat official account [开源阅读] to get Subscription source</string>
-    <string name="explore_empty">Empty now,Follow Wechat official account [开源阅读] to get Discovery source</string>
+    <string name="rss_source_empty">Empty now!</string>
+    <string name="explore_empty">Empty now!</string>
     <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
     <string name="theme_name">Theme name</string>
     <string name="auto_clear_expired">"Clear expired search histories automatically "</string>
@@ -968,5 +968,9 @@
     <string name="sys_tts_config">系统tts设置</string>
     <string name="sys_tts_config_summary">打开系统tts设置界面</string>
     <string name="cannot_timed_non_playback">非播放状态无法定时</string>
+    <string name="all_bookmark">所有书签</string>
+    <string name="change_source_batch">批量换源</string>
+    <string name="book_type_different">书籍类型不一样</string>
+    <string name="soure_change_source">是否确认换源</string>
     <!-- string end -->
 </resources>

+ 7 - 0
app/src/main/res/xml/pref_main.xml

@@ -76,6 +76,13 @@
         app:iconSpaceReserved="false"
         app:layout="@layout/view_preference_category">
 
+        <io.legado.app.ui.widget.prefs.Preference
+            android:key="bookmark"
+            android:title="@string/bookmark"
+            android:summary="@string/all_bookmark"
+            android:icon="@drawable/ic_bookmark"
+            app:iconSpaceReserved="false" />
+
         <io.legado.app.ui.widget.prefs.Preference
             android:key="readRecord"
             android:title="@string/read_record"

+ 1 - 1
gradle.properties

@@ -23,4 +23,4 @@ kotlin.code.style=official
 android.enableResourceOptimizations=true
 
 #https://chromiumdash.appspot.com/releases?platform=Android
-CronetVersion=100.0.4896.58
+CronetVersion=100.0.4896.79

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff