Merge pull request #12 from gedoor/master

Sys Code
pull/1068/head
ag2s20150909 3 years ago committed by GitHub
commit d5e7395807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/legado.yml
  2. 4
      README.md
  3. 4
      app/build.gradle
  4. 5
      app/src/debug/res/values-zh-rHK/strings.xml
  5. 5
      app/src/debug/res/values-zh-rTW/strings.xml
  6. 4
      app/src/main/AndroidManifest.xml
  7. 17
      app/src/main/assets/epub/chapter.html
  8. 22
      app/src/main/assets/epub/cover.html
  9. 267
      app/src/main/assets/epub/fonts.css
  10. 12
      app/src/main/assets/epub/intro.html
  11. BIN
      app/src/main/assets/epub/logo.png
  12. 551
      app/src/main/assets/epub/main.css
  13. 31
      app/src/main/assets/help/appHelp.md
  14. 69
      app/src/main/assets/help/ruleHelp.md
  15. 47
      app/src/main/assets/updateLog.md
  16. 15
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  17. 59
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  18. 10
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  19. 1
      app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt
  20. 20
      app/src/main/java/io/legado/app/constant/AppConst.kt
  21. 9
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  22. 2
      app/src/main/java/io/legado/app/constant/EventBus.kt
  23. 7
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  24. 31
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  25. 19
      app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
  26. 50
      app/src/main/java/io/legado/app/data/entities/Book.kt
  27. 10
      app/src/main/java/io/legado/app/data/entities/Bookmark.kt
  28. 26
      app/src/main/java/io/legado/app/help/AppConfig.kt
  29. 53
      app/src/main/java/io/legado/app/help/BookHelp.kt
  30. 16
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  31. 6
      app/src/main/java/io/legado/app/help/MediaHelp.kt
  32. 22
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  33. 4
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt
  34. 2
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  35. 2
      app/src/main/java/io/legado/app/model/Debug.kt
  36. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  37. 103
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  38. 16
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  39. 7
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  40. 10
      app/src/main/java/io/legado/app/service/DownloadService.kt
  41. 5
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  42. 20
      app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt
  43. 10
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  44. 21
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  45. 242
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  46. 37
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  47. 28
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  48. 18
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  49. 21
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  50. 19
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  51. 21
      app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt
  52. 18
      app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
  53. 15
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  54. 12
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  55. 46
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  56. 4
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  57. 12
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  58. 34
      app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt
  59. 12
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt
  60. 64
      app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
  61. 2
      app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt
  62. 18
      app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt
  63. 7
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  64. 2
      app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt
  65. 149
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  66. 140
      app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt
  67. 278
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  68. 82
      app/src/main/java/io/legado/app/ui/main/bookshelf/RootView.kt
  69. 166
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt
  70. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt
  71. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt
  72. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt
  73. 4
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
  74. 85
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt
  75. 134
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt
  76. 150
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt
  77. 235
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
  78. 19
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt
  79. 47
      app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt
  80. 2
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  81. 12
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  82. 59
      app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt
  83. 48
      app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt
  84. 35
      app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt
  85. 31
      app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt
  86. 8
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  87. 37
      app/src/main/java/io/legado/app/utils/HtmlFormatter.kt
  88. 29
      app/src/main/java/io/legado/app/web/HttpServer.kt
  89. 2
      app/src/main/res/color/selector_image.xml
  90. 2
      app/src/main/res/layout/activity_book_source_edit.xml
  91. 2
      app/src/main/res/layout/activity_chapter_list.xml
  92. 2
      app/src/main/res/layout/activity_explore_show.xml
  93. 2
      app/src/main/res/layout/activity_main.xml
  94. 2
      app/src/main/res/layout/activity_rss_artivles.xml
  95. 45
      app/src/main/res/layout/dialog_bookshelf_config.xml
  96. 47
      app/src/main/res/layout/dialog_page_key.xml
  97. 42
      app/src/main/res/layout/dialog_tip_config.xml
  98. 6
      app/src/main/res/layout/fragment_bookshelf.xml
  99. 48
      app/src/main/res/layout/fragment_bookshelf1.xml
  100. 4
      app/src/main/res/layout/fragment_explore.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -76,5 +76,5 @@ jobs:
done
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzous.com/b0f810h4b"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b"

@ -45,9 +45,13 @@ com.positiondev.epublib:epublib-core
## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html
## 界面
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B4.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg)
## 其它
其它网友做的IOS版本: https://github.com/kaich/Yuedu

@ -37,7 +37,7 @@ android {
multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = [
arguments += [
"room.incremental" : "true",
"room.expandProjection": "true",
"room.schemaLocation" : "$projectDir/schemas".toString()
@ -181,7 +181,7 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
//Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
//webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1'

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜索</string>
</resources>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜尋</string>
</resources>

@ -246,7 +246,7 @@
android:screenOrientation="behind" />
<!-- 目录 -->
<activity
android:name=".ui.book.toc.ChapterListActivity"
android:name=".ui.book.toc.TocActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- 正文搜索 -->
@ -286,7 +286,7 @@
android:launchMode="singleTop" />
<!-- 书源登录 -->
<activity
android:name=".ui.login.SourceLogin"
android:name=".ui.login.SourceLoginActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 阅读记录 -->

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Chapter</title>
<link href="../Styles/fonts.css" type="text/css" rel="stylesheet"/>
<link href="../Styles/main.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<div class="logo">
<img alt="" class="logo" src="../Images/logo.png"/>
</div>
<h2 class="head">{title}</h2>
{content}
</body>
</html>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Cover</title>
<style type="text/css">
.pic {
margin: 50% 30% 0 30%;
padding: 2px 2px;
border: 1px solid #f5f5dc;
background-color: rgba(250,250,250, 0);
border-radius: 1px;
}
</style>
</head>
<body style="text-align: center;">
<div class="pic"><img src="../Images/cover.jpg" style="width: 100%; height: auto;"/></div>
<h1 style="margin-top: 5%; font-size: 110%;">{name}</h1>
<div class="author" style="margin-top: 0;"><b>{author}</b> <span style="font-size: smaller;">/ 著</span></div>
</body>
</html>

@ -0,0 +1,267 @@
@charset "utf-8";
/*---常用---*/
@font-face {
font-family: "zw";
src:
local("宋体"),local("明体"),local("明朝"),
local("Songti"),local("Songti SC"),local("Songti TC"), /*iOS6+iBooks3*/
local("Song S"),local("Song T"),local("STBShusong"),local("TBMincho"),local("HYMyeongJo"), /*Kindle Paperwihite*/
local("DK-SONGTI"),
url(../Fonts/zw.ttf),
url(res:///opt/sony/ebook/FONT/zw.ttf),
url(res:///Data/FONT/zw.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/zw.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/zw.ttf),
url(res:///ebook/fonts/zw.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/zw.ttf),
url(res:///../../media/mmcblk0p1/fonts/zw.ttf),
url(file:///mnt/us/DK_System/system/fonts/zw.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/zw.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/zw.ttf),
url(res:///system/fonts/zw.ttf),
url(res:///system/media/sdcard/fonts/zw.ttf),
url(res:///media/fonts/zw.ttf),
url(res:///sdcard/fonts/zw.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/zw.ttf),
url(res:///media/flash/fonts/zw.ttf),
url(res:///media/sd/fonts/zw.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/zw.ttf),
url(res:///../fonts/zw.ttf),
url(../../../../../zw.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/zw.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/zw.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/zw.ttf);
/*ADE1,8, 2.0 Windows Path*/;
}
@font-face {
font-family: "fs";
src:
local("amasis30"),local("仿宋"),local("仿宋_GB2312"),
local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), /*iOS6+iBooks3*/
local("DK-FANGSONG"),
url(../Fonts/fs.ttf),
url(res:///opt/sony/ebook/FONT/fs.ttf),
url(res:///Data/FONT/fs.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/fs.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/fs.ttf),
url(res:///ebook/fonts/fs.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/fs.ttf),
url(res:///../../media/mmcblk0p1/fonts/fs.ttf),
url(file:///mnt/us/DK_System/system/fonts/fs.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/fs.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/fs.ttf),
url(res:///system/fonts/fs.ttf),
url(res:///system/media/sdcard/fonts/fs.ttf),
url(res:///media/fonts/fs.ttf),
url(res:///sdcard/fonts/fs.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/fs.ttf),
url(res:///media/flash/fonts/fs.ttf),
url(res:///media/sd/fonts/fs.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/fs.ttf),
url(res:///../fonts/fs.ttf),
url(../../../../../fs.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/fs.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/fs.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/fs.ttf);
/*ADE1,8, 2.0 Windows Path*/;
}
@font-face {
font-family: "kt";
src:
local("Caecilia"),local("楷体"),local("楷体_GB2312"),
local("Kaiti"),local("Kaiti SC"),local("Kaiti TC"), /*iOS6+iBooks3*/
local("MKai PRC"),local("MKaiGB18030C-Medium"),local("MKaiGB18030C-Bold"), /*Kindle Paperwihite*/
local("DK-KAITI"),
url(../Fonts/kt.ttf),
url(res:///opt/sony/ebook/FONT/kt.ttf),
url(res:///Data/FONT/kt.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/kt.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/kt.ttf),
url(res:///ebook/fonts/kt.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/kt.ttf),
url(res:///../../media/mmcblk0p1/fonts/kt.ttf),
url(file:///mnt/us/DK_System/system/fonts/kt.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/kt.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/kt.ttf),
url(res:///system/fonts/kt.ttf),
url(res:///system/media/sdcard/fonts/kt.ttf),
url(res:///media/fonts/kt.ttf),
url(res:///sdcard/fonts/kt.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/kt.ttf),
url(res:///media/flash/fonts/kt.ttf),
url(res:///media/sd/fonts/kt.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/kt.ttf),
url(res:///../fonts/kt.ttf),
url(../../../../../kt.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/kt.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/kt.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/kt.ttf);
/*ADE1,8, 2.0 Windows Path*/;
}
@font-face {
font-family: "ht";
src:
local("黑体"),local("微软雅黑"),
local("Heiti"),local("Heiti SC"),local("Heiti TC"), /*iOS6+iBooks3*/
local("MYing Hei S"),local("MYing Hei T"),local("TBGothic"), /*Kindle Paperwihite*/
local("DK-HEITI"),
url(../Fonts/ht.ttf),
url(res:///opt/sony/ebook/FONT/ht.ttf),
url(res:///Data/FONT/ht.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/ht.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/ht.ttf),
url(res:///ebook/fonts/ht.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/ht.ttf),
url(res:///../../media/mmcblk0p1/fonts/ht.ttf),
url(file:///mnt/us/DK_System/system/fonts/ht.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/ht.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/ht.ttf),
url(res:///system/fonts/ht.ttf),
url(res:///system/media/sdcard/fonts/ht.ttf),
url(res:///media/fonts/ht.ttf),
url(res:///sdcard/fonts/ht.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/ht.ttf),
url(res:///media/flash/fonts/ht.ttf),
url(res:///media/sd/fonts/ht.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/ht.ttf),
url(res:///../fonts/ht.ttf),
url(../../../../../ht.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/ht.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/ht.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/ht.ttf);
/*ADE1,8, 2.0 Windows Path*/;
}
@font-face {
font-family:"h1";
src:
local("方正兰亭特黑长_GBK"),local("方正兰亭特黑长简体"),local("方正兰亭特黑长繁体"),
local("LantingTeheichang"),
local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"),
local("DK-HEITI"),
url(../Fonts/h1.ttf),
url(res:///opt/sony/ebook/FONT/h1.ttf),
url(res:///Data/FONT/h1.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/h1.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/h1.ttf),
url(res:///ebook/fonts/h1.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/h1.ttf),
url(res:///../../media/mmcblk0p1/fonts/h1.ttf),
url(file:///mnt/us/DK_System/system/fonts/h1.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/h1.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/h1.ttf),
url(res:///system/fonts/h1.ttf),
url(res:///system/media/sdcard/fonts/h1.ttf),
url(res:///media/fonts/h1.ttf),
url(res:///sdcard/fonts/h1.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/h1.ttf),
url(res:///media/flash/fonts/h1.ttf),
url(res:///media/sd/fonts/h1.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/h1.ttf),
url(res:///../fonts/h1.ttf),
url(../../../../../h1.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/h1.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/h1.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/h1.ttf); /*ADE1,8, 2.0 Windows Path*/
}
@font-face {
font-family:"h2";
src:
local("方正大标宋_GBK"),local("方正大标宋简体"),local("方正大标宋繁体"),
local("Dabiaosong"),
local("Heiti"),local("Heiti SC"),local("Heiti TC"),
local("DK-XIAOBIAOSONG"),
url(../Fonts/h2.ttf),
url(res:///opt/sony/ebook/FONT/h2.ttf),
url(res:///Data/FONT/h2.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/h2.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/h2.ttf),
url(res:///ebook/fonts/h2.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/h2.ttf),
url(res:///../../media/mmcblk0p1/fonts/h2.ttf),
url(file:///mnt/us/DK_System/system/fonts/h2.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/h2.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/h2.ttf),
url(res:///system/fonts/h2.ttf),
url(res:///system/media/sdcard/fonts/h2.ttf),
url(res:///media/fonts/h2.ttf),
url(res:///sdcard/fonts/h2.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/h2.ttf),
url(res:///media/flash/fonts/h2.ttf),
url(res:///media/sd/fonts/h2.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/h2.ttf),
url(res:///../fonts/h2.ttf),
url(../../../../../h2.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/h2.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/h2.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/h2.ttf); /*ADE1,8, 2.0 Windows Path*/
}
@font-face {
font-family:"h3";
src:
local("方正华隶_GBK"),local("方正行黑简体"),local("方正行黑繁体"),
local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"),
local("DK-FANGSONG"),
url(../Fonts/h3.ttf),
url(res:///opt/sony/ebook/FONT/h3.ttf),
url(res:///Data/FONT/h3.ttf),
url(res:///opt/sony/ebook/FONT/tt0011m_.ttf),
url(res:///ebook/fonts/../../mnt/sdcard/fonts/h3.ttf),
url(res:///ebook/fonts/../../mnt/extsd/fonts/h3.ttf),
url(res:///ebook/fonts/h3.ttf),
url(res:///ebook/fonts/DroidSansFallback.ttf),
url(res:///fonts/ttf/h3.ttf),
url(res:///../../media/mmcblk0p1/fonts/h3.ttf),
url(file:///mnt/us/DK_System/system/fonts/h3.ttf), /*Duokan Old Path*/
url(file:///mnt/us/DK_System/xKindle/res/userfonts/h3.ttf), /*Duokan 2012 Path*/
url(res:///abook/fonts/h3.ttf),
url(res:///system/fonts/h3.ttf),
url(res:///system/media/sdcard/fonts/h3.ttf),
url(res:///media/fonts/h3.ttf),
url(res:///sdcard/fonts/h3.ttf),
url(res:///system/fonts/DroidSansFallback.ttf),
url(res:///mnt/MOVIFAT/font/h3.ttf),
url(res:///media/flash/fonts/h3.ttf),
url(res:///media/sd/fonts/h3.ttf),
url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),
url(res:///../../fonts/h3.ttf),
url(res:///../fonts/h3.ttf),
url(../../../../../h3.ttf), /*EpubReaderI*/
url(res:///mnt/sdcard/fonts/h3.ttf), /*Nook for Android: fonts in TF Card*/
url(res:///fonts/h3.ttf), /*ADE1,8, 2.0 Program Path*/
url(res:///../../../../Windows/fonts/h3.ttf); /*ADE1,8, 2.0 Windows Path*/
}
@font-face {
font-family:"luohua";
src:local("汉仪落花体"),
url("../Fonts/hylh.ttf");
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
<title>Intro</title>
<link href="../Styles/fonts.css" type="text/css" rel="stylesheet" />
<link href="../Styles/main.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h1 class="head" style="margin-bottom:2em;">内容简介</h1>{intro}</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

@ -0,0 +1,551 @@
@charset "utf-8";
@import url("../Styles/fonts.css");
body {
padding: 0%;
margin-top: 0%;
margin-bottom: 0%;
margin-left: 0.5%;
margin-right: 0.5%;
line-height: 130%;
text-align: justify;
font-family: "DK-SONGTI","st","宋体","zw",sans-serif;
}
p {
text-align: justify;
text-indent: 2em;
line-height: 130%;
margin-right: 0.5%;
margin-left: 0.5%;
font-family: "DK-SONGTI","st","宋体","zw",sans-serif;
}
p.kaiti {
font-family: "DK-KAITI","kt","楷体","zw",serif;
}
p.fangsong {
font-family: "DK-FANGSONG","fs","仿宋","zw",serif;
}
span.xinli {
font-family: "DK-KAITI","kt","楷体","zw",serif;
color: #4e753f;
}
/** 英文斜体字 **/
span.english{
font-style: italic;
}
div {
margin: 0px;
padding: 0px;
line-height: 120%;
text-align: justify;
font-family: "zw";
}
div.foot {
text-indent: 2em;
margin: 30% 5% 0 5%;
padding: 8px 0;
}
p.foot {
font-family: "DK-KAITI","kt","楷体","zw",serif;
}
/*扉页*/
.booksubtitle {
padding: 10px 0 0px 0;
text-indent: 0em;
font-size: 75%;
font-family: "ht";
}
.booktitle {
padding: 9% 0 0 0;
font-size: 1.3em;
font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG";
font-weight: normal;
text-indent: 0em;
color: #000;
text-align: center;
line-height: 1.6;
}
.booktitle0 {
font-size: 1.2em;
font-family: "fs";
text-indent: 0em;
text-align: center;
line-height: 1.8;
}
.booktitle1 {
padding: 0 0 0 0;
font-size: 0.85em;
font-family: "fs";
text-indent: 0em;
text-align: center;
line-height: 1.6;
}
.bookauthor {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
padding: 5% 5px 0px 5px;
text-indent: 0em;
text-align: center;
color: #000;
font-size: 90%;
line-height: 1.3;
}
.booktranslator {
padding: 1% 5px 0px 5px;
text-indent: 0em;
text-align: center;
font-size: 85%;
line-height: 1.3;
}
.bookpub {
font-family: "DK-KAITI","kt","楷体","楷体_gb2312";
padding: 30% 5px 5px 5px;
text-indent: 0em;
color: #000;
text-align: center;
font-size: 80%;
}
/*标题页*/
body.head {
background-repeat:no-repeat no-repeat;
background-size:160px 229px;
background-position:bottom right;
background-attachment:fixed;
}
body.xhead {
background-color: #FDF5E6;
}
h1.head {
font-family: "DK-HEITI",黑体,sans-serif;
font-size: 1.2em;
font-weight: bold;
color: #311a02;
text-indent: 0em;
font-weight: normal;
duokan-text-indent: 0em;
padding: auto;
text-align: center;
margin-top: -8em;
}
div.head {
border: solid 2px #ffffff;
padding: 2px;
margin: 2em auto 0.7em auto;
text-align: center;
width: 1em;
}
h1.head b {
font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG";
font-weight: bold;
font-size: 1.2em;
text-align: center;
text-indent: 0em;
duokan-text-indent: 0em;
color: #311a02;
margin: 0.5em auto;
line-height: 140%;
}
div.back {
text-align: center;
text-indent: 0em;
duokan-text-indent: 0em;
margin: 4em auto;
}
img.back {
width: 70%;
}
img.back2 {
width: 40%;
margin: 2em 0 0 0;
}
/*正文*/
/**楷体引文**/
.titou {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
}
.yinwen {
font-family: "DK-KAITI","kt","楷体","zw",serif;
margin-left: 2em;
text-indent: 0em;
}
.nicename {
font-family: "DK-HEITI",黑体,sans-serif;
font-weight: bold;
font-size: 0.9em;
}
body.head3 {
background-color: #a7bdcc;
color: #354f66;
}
body.head4 {
background-color: #bfd19b;
color: #4e753f;
}
h2.head {
font-family: "小标宋";
text-align: left;
font-weight: bold;
font-size: 1.1em;
margin: -3em 2em 2em 0;
color: #3f83e8;
line-height: 140%;
}
h2.head span {
font-family: "仿宋";
font-size: 0.7em;
background-color: #3f83e8;
border-radius: 9px;
padding: 4px;
color: #fff;
}
div.logo {
margin: -2em 0% 0 0;
text-align: right;
}
img.logo {
width: 40%;
}
.imgl {
/*图片居右*/
margin: -8.8em 1em 4em 0em;
width: 80%;
text-align: right;
}
h1.head {
line-height:130%;
font-size:1.4em;
text-align: center;
color: #BA2213;
font-weight: bold;
margin-top: 2em;
margin-bottom: 1em;
font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG";
}
h3 {
font-family: "DK-HEITI",黑体,sans-serif;
font-size: 1.1em;
margin: 1em 0;
border-left: 1.2em solid #00a1e9;
line-height: 120%;
padding-left: 3px;
color: #00a1e9;
}
h4 {
font-family: "DK-HEITI",黑体,sans-serif;
font-size: 1.1em;
text-align: center;
margin: 1em 0;
line-height: 120%;
color: #000;
}
h1.post {
font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG";
text-align: center;
font-size: 1.3em;
color: #026fca;
margin: 3em auto 2em auto;
}
.banquan {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
text-align: left;
color: #000;
font-size:1.1em;
margin-bottom:1em;
text-indent: 1em;
duokan-text-indent: 1em;
}
p.post {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
}
p.zy {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
margin: 1em 0 0 1em;
padding: 5px 0px 5px 10px;
text-indent: 0em;
border-left: 5px solid #a9b5c1;
}
.sign {
font-family: "DK-KAITI","kt","楷体","zw",serif;
margin: 1em 2px 0 auto;
text-align: right;
font-size: 0.8em;
text-indent: 0em;
duokan-text-indent: 0em;
}
.mark {
font-family: "DK-HEITI",黑体,sans-serif;
font-size: 0.9em;
color: #fff;
text-indent: 0em;
duokan-text-indent: 0em;
background-color: maroon;
text-align: center;
padding: 0px;
margin: 2em 30%;
}
/*出版社*/
.chubanshe img{
width:106px;
height:28px;
}
.chubanshe {
margin-top:20px;
}
.cr {
font-size:0.9em;
}
/*多看画廊*/
div.duokan-image-single {
text-align: center;
margin: 0.5em auto; /*插图盒子上下外边距为0.5em,左右设置auto是为了水平居中这个盒子*/
}
img.picture-80 {
margin: 0; /*清除img元素的外边距*/
width: 80%; /*预览窗口的宽度*/
box-shadow: 3px 3px 10px #bfbfbf; /*给图片添加阴影效果*/
}
p.duokan-image-maintitle {
margin: 1em 0 0; /*图片说明的段间距*/
font-family: "楷体"; /*图片说明使用的字体*/
font-size: 0.9em; /*字体大小*/
text-indent: 0; /*首行缩进为零,当你使用单标签p来指定首行缩进为2em时,记得在需要居中的文本中清除缩进,因为样式是叠加的*/
text-align: center; /*图片说明水平居中*/
color: #a52a2a; /*字体颜色*/
line-height: 1.25em; /*行高,防止有很长的图片说明*/
}
/*制作说明页*/
body.description {
background-image: url(../Images/001.png);
background-position: bottom center;
background-repeat: no-repeat;
background-size: cover;
padding: 25% 10% 0;
font-size: 0.9em;
}
div.description-body {
width: 55%;
padding: 2em 1.3em;
border-radius: 0.5em;
font-size: 0.9em;
border-style: solid;
border-color: #393939;
border-width: 0.3em;
border-radius: 5em;
background-color: #5a5a5a;
box-shadow: 2px 2px 3px #828281;
}
h1.description-title {
text-align: center;
font-family: "黑体";
font-size: 1.2em;
margin: 0 0 1em 0;
color: #FF9;
text-shadow: 1px 1px 0 black;
}
p.description-text {
color: #f9ddd2;
font-family: "准圆";
margin: 0;
text-align: justify;
text-indent: 0;
duokan-text-indent: 0;
}
hr.description-hr {
margin: 0.5em -1em;
border-style: dotted;
border-color: #9C9;
border-width: 0.05em 0 0 0;
}
p.tips {
text-align: justify;
text-indent: 0;
duokan-text-indent: 0;
font-family: "楷体";
font-size: 0.7em;
color: #FFC;
margin: 0;
}
/*版本说明页*/
.ver {
font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif;
font-weight: bold;
font-size: 100%;
color: #000;
margin: 1em 0 1em 0;
text-align: center;
}
.vertitle {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
font-size: 100%;
text-indent: 0em;
text-align: left;
duokan-text-indent: 0em;
}
.vertxt {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
line-height: 100%;
font-size: 85%;
text-indent: 0em;
text-align: left;
duokan-text-indent: 0em;
}
.verchar {
font-family: "DK-KAITI","kt","楷体","楷体_gb2312";
text-align: left;
text-indent: 1em;
duokan-text-indent: 1em;
margin-bottom: 1em;
margin-top: 1em;
}
.vernote {
font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif;
font-size: 75%;
color: #686d70;
text-indent: 0em;
text-align: left;
duokan-text-indent: 0em;
padding-bottom: 15px;
}
.line {
border: dotted #A2906A;
border-width: 1px 0 0 0;
}
.entry {
margin-left: 18px;
font-size: 83%;
color: #8fe0a3;
text-indent: 0em;
duokan-text-indent: 0em;
}
/*版权信息*/
.vol {
text-indent: 0em;
text-align: center;
padding: 0.8em;
margin: 0 auto 3px auto;
color: #000;
font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG";
font-size: 130%;
text-shadow: none;
}
.cp {
font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif;
color: #412938;
font-size: 70%;
text-align: left;
text-indent: 0em;
duokan-text-indent: 0em;
}
.xchar {
font-family: "DK-KAITI","kt","楷体","楷体_gb2312";
text-indent: 0em;
duokan-text-indent: 0em;
}
/*多看弹注*/
sup img {
line-height: 100%;
width: auto;
height: 1.0em;
margin: 0em;
padding: 0em;
vertical-align: text-top;
}
ol {
margin-bottom:0;
padding:0 auto;
list-style-type: decimal;
}
.hr {
width:50%;
margin:2em 0 0 0.5em;
padding:0;
height:2px;
background-color: #F3221D;
}
.duokan-footnote-content{
padding:0 auto;
text-align: left;
}
.duokan-footnote-item {
font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif;
text-align: left;
font-size: 80%;
line-height: 100%;
clear: both;
color:#000;
list-style-type:decimal;
}
li.duokan-footnote-item a {
font-family:"DK-HEITI";
text-align: left;
}
a{
text-decoration: none;
color: #222;
}
a:hover {background: #81caf9}
a:active {background: yellow}
.duokan-image-maintitle {
font-family:"DK-HEITI",黑体,"hei",sans-serif;
text-align: center;
text-indent: 0em;
duokan-text-indent: 0em;
font-size:90%;
color: #1F4150;
margin-top: 1em;
}
.duokan-image-subtitle {
font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif;
text-align: center;
text-indent: 0em;
duokan-text-indent: 0em;
font-size:70%;
color: #3A3348;
margin-top: 1em;
}

@ -10,10 +10,7 @@
2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理?
* 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。
3. “勐”、“收十”、“猫腻”、“发动机”、“沃尔沃”等词无法替换净化,如何解决?
* 请在 界面 - 简/繁 中关闭简繁互转功能.
4. 漫画源看书显示乱码,如何解决?
3. 漫画源看书显示乱码,如何解决?
* 异次元和阅读是两个不同的软件,**两个软件的源并不通用**,请导入阅读的支持的漫画源!
## 书源相关
@ -28,7 +25,7 @@
* 导入后返回书源管理界面;
* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/
![导入书源](https://gitee.com/gekunfei/web/raw/master/legado/importSource.jpg)
![QQ导入书源](https://cdn.jsdelivr.net/gh/gedoor/gedoor.github.io@master/images/importSource.jpg)
2. 如何新建大佬发的单独书源?
* 复制书源代码;
@ -54,7 +51,7 @@
* 找到myBookSource.json长按选择分享;
* 选择微信分享或者QQ分享;
* 选择你要分享的好友点击发送;
* 好友接收后在手机自带内存根目录找到myBookSource.json文件(QQ 安卓10及以下版本在Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/,安卓11版本用户由于系统限制无法访问data目录,微信在Tencent/MicroMsg/Download);
* 好友接收后在手机自带内存根目录找到myBookSource.json文件(最新版QQ 安卓10及以下版本在Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/,安卓11版本用户由于系统限制无法访问data目录,微信在Tencent/MicroMsg/Download);
* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹);
* 打开阅读软件点击恢复。
* 注:备份路径如已修改过请在修改后的路径下查找书源文件。
@ -70,7 +67,7 @@
## 本地书籍相关
1. 目前阅读支持哪些格式的本地书籍?
* 目前支持TXT、EPUB格式(只支持显示EPUB里的文本内容,还不支持显示图片)
* 目前支持TXT、EPUB格式
2. 如何导入本地书籍?
* 在书架页面点击右上角,选择“添加本地”,授予相关权限后即可导入本地书籍。也可在文件管理器中使用 **阅读** 打开相关书籍。
@ -83,10 +80,22 @@
4. 如何下载书籍到本地?
* 把在线书籍加入到书架后,在书架页面点击右上角,选择“离线缓存”即可。
5. 为什么我打开本地的TXT文件,显示内容却是乱码?
5. 如何自定义导出的txt/epub文件名称?
* 点击 **离线缓存** - **导出文件名**.
* 使用方法:
- 导出文件名支持js语法
- 可用变量: name - 书名 author-作者
- 示例:
> {name+"作者:"+author}
- 导出文件名:
> Legado是最好的在线阅读软件 作者: kunfei.
**注意:** name、author等变量与字符串的拼接都需要在js环境中进行,即必须使用{ } 将变量与字符串包裹起来.
6. 为什么我打开本地的TXT文件,显示内容却是乱码?
* 部分编码在阅读上会识别错误,建议先用文本编辑器转换为常用的UTF-8格式。
6. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决?
7. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决?
* 点击右上角更换目录规则即可。
## 书籍界面相关
@ -131,7 +140,9 @@
* 请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的 **⁝** 按钮,在弹出的菜单中,选择 **翻页动画(本书)**,将翻页动画更改为 **滚动**
14. 阅读图片章节、漫画或epub插图时,图片被缩放到一页中,以至无法看清,如何处理?
* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考Q13中的方案将翻页动画更改为**滚动**,再点击书籍界面的章节标题进入 编辑书源 界面,在 正文-图片样式 中填入 *full*,保存更改,刷新当前章节即可。
* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考Q13中的方案将翻页动画更改为**滚动**
* 3.0旧版可以点击书籍界面的章节标题进入 **编辑书源** 界面,在 正文-图片样式 中填入 *full*,保存更改,刷新当前章节即可。
* 3.0新版可以直接在书籍阅读界面点击右上角的 **⁝** 按钮,选择 图片样式- *full*.
## 替换净化相关

@ -11,6 +11,12 @@
: regex规则,不可省略,只可以用在书籍列表和目录列表
```
* 获取登录后的cookie
```
java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd
java.getCookie("http://baidu.com", "userid") => 1234
```
* 请求头,支持http代理,socks4 socks5代理设置
```
socks5代理
@ -39,6 +45,49 @@ cache 变量-缓存操作类,方法见 io.legado.app.help.CacheManager
chapter 变量-当前目录类,方法见 io.legado.app.data.entities.BookChapter
title 变量-当前标题,String
src 内容,源码
```
* url添加js参数,解析url时执行,可在访问url时处理url,例
```
https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"}
https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"}
```
* 增加js方法,用于重定向拦截
* `java.get(urlStr: String, headers: Map<String, String>)`
* `java.post(urlStr: String, body: String, headers: Map<String, String>)`
* 对于搜索重定向的源,可以使用此方法获得重定向后的url
```
(()=>{
if(page==1){
let url='https://www.yooread.net/e/search/index.php,'+JSON.stringify({
"method":"POST",
"body":"show=title&tempid=1&keyboard="+key
});
return java.put('surl',String(java.connect(url).raw().request().url()));
} else {
return java.get('surl')+'&page='+(page-1)
}
})()
或者
(()=>{
let base='https://www.yooread.net/e/search/';
if(page==1){
let url=base+'index.php';
let body='show=title&tempid=1&keyboard='+key;
return base+java.put('surl',java.post(url,body,{}).header("Location"));
} else {
return base+java.get('surl')+'&page='+(page-1);
}
})()
```
* 正文图片链接支持修改headers
```
let options = {
"headers": {"User-Agent": "xxxx","Referrer":baseUrl,"Cookie":"aaa=vbbb;"}
};
'<img src="'+src+","+JSON.stringify(options)+'">'
```
## 部分js对象属性说明
@ -95,14 +144,16 @@ variable // 自定义书籍变量信息(用于书源规则检索书籍信息)
### 字体解析使用
> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码
```
@js:
var b64=String(src).match(/ttf;base64,([^\)]+)/);
if (b64) {
var f1 = java.queryBase64TTF(b64[1])
var f2 = java.queryTTF("/storage/emulated/0/Fonts/Source Han Sans CN Regular.ttf")
java.replaceFont(result, f1, f2)
}else{
result
}
<js>
(function(){
var b64=String(src).match(/ttf;base64,([^\)]+)/);
if(b64){
var f1 = java.queryBase64TTF(b64[1]);
var f2 = java.queryTTF("https://alanskycn.gitee.io/teachme/assets/font/Source Han Sans CN Regular.ttf");
return java.replaceFont(result, f1, f2);
}
return result;
})()
</js>
```

@ -2,6 +2,53 @@
* 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
## **必读**
【温馨提醒】 *更新前一定要做好备份,以免数据丢失!*
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题。先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
**2021/06/15**
* viewPager2 改回 viewPager
* 添加配置导入文件规则功能 by bushixuanqi
* 文件夹分组样式优化(未完成)
**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/26**
* 书签绑定书名与作者
* 修复详情页目录问题
**2021/05/24**
* 反转目录后刷新内容
* 修复上下滑动会导致左右切换问题
* 精确搜索增加包含关键词的,比如搜索五行 五行天也显示出来, 五天行不显示
**2021/05/21**
* 添加反转目录功能
* 修复分享bug
* 详情页添加登录菜单
* 添加发现界面隐藏配置
**2021/05/16**
* 添加总是使用默认封面配置
* 添加一种语言 ptbr translation by mezysinc
* epublib 修bug by ag2s20150909
**2021/05/12**
* 修复预下载bug

@ -10,7 +10,7 @@ import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import com.google.gson.Gson
import io.legado.app.api.controller.BookshelfController
import io.legado.app.api.controller.BookController
import io.legado.app.api.controller.SourceController
import java.util.*
@ -19,7 +19,8 @@ import java.util.*
*/
class ReaderProvider : ContentProvider() {
private enum class RequestCode {
SaveSource, SaveSources, SaveBook, DeleteSources, GetSource, GetSources, GetBookshelf, GetChapterList, GetBookContent
SaveSource, SaveSources, SaveBook, DeleteSources, GetSource, GetSources,
GetBookshelf, RefreshToc, GetChapterList, GetBookContent
}
private val postBodyKey = "json"
@ -33,6 +34,7 @@ class ReaderProvider : ContentProvider() {
addURI(authority, "source/query", RequestCode.GetSource.ordinal)
addURI(authority, "sources/query", RequestCode.GetSources.ordinal)
addURI(authority, "books/query", RequestCode.GetBookshelf.ordinal)
addURI(authority, "book/refreshToc/query", RequestCode.RefreshToc.ordinal)
addURI(authority, "book/chapter/query", RequestCode.GetChapterList.ordinal)
addURI(authority, "book/content/query", RequestCode.GetBookContent.ordinal)
}
@ -65,7 +67,7 @@ class ReaderProvider : ContentProvider() {
SourceController.saveSource(values.getAsString(postBodyKey))
}
RequestCode.SaveBook -> values?.let {
BookshelfController.saveBook(values.getAsString(postBodyKey))
BookController.saveBook(values.getAsString(postBodyKey))
}
RequestCode.SaveSources -> values?.let {
SourceController.saveSources(values.getAsString(postBodyKey))
@ -91,9 +93,10 @@ class ReaderProvider : ContentProvider() {
return if (sMatcher.match(uri) < 0) null else when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.GetSource -> SimpleCursor(SourceController.getSource(map))
RequestCode.GetSources -> SimpleCursor(SourceController.sources)
RequestCode.GetBookshelf -> SimpleCursor(BookshelfController.bookshelf)
RequestCode.GetBookContent -> SimpleCursor(BookshelfController.getBookContent(map))
RequestCode.GetChapterList -> SimpleCursor(BookshelfController.getChapterList(map))
RequestCode.GetBookshelf -> SimpleCursor(BookController.bookshelf)
RequestCode.GetBookContent -> SimpleCursor(BookController.getBookContent(map))
RequestCode.RefreshToc -> SimpleCursor(BookController.refreshToc(map))
RequestCode.GetChapterList -> SimpleCursor(BookController.getChapterList(map))
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
)

@ -1,10 +1,12 @@
package io.legado.app.api.controller
import io.legado.app.R
import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON
@ -14,8 +16,11 @@ import io.legado.app.utils.getPrefInt
import kotlinx.coroutines.runBlocking
import splitties.init.appCtx
object BookshelfController {
object BookController {
/**
* 书架所有书籍
*/
val bookshelf: ReturnData
get() {
val books = appDb.bookDao.all
@ -35,6 +40,55 @@ object BookshelfController {
}
}
/**
* 更新目录
*/
fun refreshToc(parameters: Map<String, List<String>>): ReturnData {
val returnData = ReturnData()
try {
val bookUrl = parameters["url"]?.getOrNull(0)
if (bookUrl.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
}
val book = appDb.bookDao.getBook(bookUrl)
?: return returnData.setErrorMsg("bookUrl不对")
if (book.isLocalBook()) {
val toc = LocalBook.getChapterList(book)
appDb.bookChapterDao.delByBook(book.bookUrl)
appDb.bookChapterDao.insert(*toc.toTypedArray())
appDb.bookDao.update(book)
return if (toc.isEmpty()) {
returnData.setErrorMsg(appCtx.getString(R.string.error_load_toc))
} else {
returnData.setData(toc)
}
} else {
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
?: return returnData.setErrorMsg("未找到对应书源,请换源")
val webBook = WebBook(bookSource)
val toc = runBlocking {
if (book.tocUrl.isBlank()) {
webBook.getBookInfoAwait(this, book)
}
webBook.getChapterListAwait(this, book)
}
appDb.bookChapterDao.delByBook(book.bookUrl)
appDb.bookChapterDao.insert(*toc.toTypedArray())
appDb.bookDao.update(book)
return if (toc.isEmpty()) {
returnData.setErrorMsg(appCtx.getString(R.string.error_load_toc))
} else {
returnData.setData(toc)
}
}
} catch (e: Exception) {
return returnData.setErrorMsg(e.localizedMessage ?: "refresh toc error")
}
}
/**
* 获取目录
*/
fun getChapterList(parameters: Map<String, List<String>>): ReturnData {
val bookUrl = parameters["url"]?.getOrNull(0)
val returnData = ReturnData()
@ -45,6 +99,9 @@ object BookshelfController {
return returnData.setData(chapterList)
}
/**
* 获取正文
*/
fun getBookContent(parameters: Map<String, List<String>>): ReturnData {
val bookUrl = parameters["url"]?.getOrNull(0)
val index = parameters["index"]?.getOrNull(0)?.toInt()

@ -175,12 +175,14 @@ abstract class BaseActivity<VB : ViewBinding>(
}
}
if (imageBg) {
ThemeConfig.getBgImage(this)?.let {
try {
try {
ThemeConfig.getBgImage(this)?.let {
window.decorView.background = it
} catch (e: OutOfMemoryError) {
toastOnUi("Image Bg Out Of Memory")
}
} catch (e: OutOfMemoryError) {
toastOnUi(e.localizedMessage)
} catch (e: Exception) {
toastOnUi(e.localizedMessage)
}
}
}

@ -53,6 +53,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
)
}
}
@Suppress("DEPRECATION")
f.setTargetFragment(this, 0)
f.show(parentFragmentManager, dialogFragmentTag)

@ -2,6 +2,7 @@ package io.legado.app.constant
import android.annotation.SuppressLint
import android.provider.Settings
import io.legado.app.BuildConfig
import io.legado.app.R
import splitties.init.appCtx
import java.text.SimpleDateFormat
@ -37,9 +38,9 @@ object AppConst {
val keyboardToolChars: List<String> by lazy {
arrayListOf(
"", "@css:", "<js></js>", "{{}}", "&&", "%%", "||", "//", "$.", "@",
"\\", ":", "class", "id", "href", "textNodes", "ownText", "all", "html",
"[", "]", "<", ">", "#", "!", ".", "+", "-", "*", "="
"", "@css:", "<js></js>", "{{}}", "##", "&&", "%%", "||", "//", "$.",
"@", ":", "class", "text", "href", "textNodes", "ownText", "all", "html",
"[", "]", "<", ">", "#", "!", ".", "+", "-", "*", "=", "{'webView': true}"
)
}
@ -56,10 +57,12 @@ object AppConst {
val urlOption: String by lazy {
"""
,{
"charset": "",
"method": "POST",
"body": "",
"headers": {"User-Agent": ""}
'charset': '',
'method': 'POST',
'body': '',
'headers': {
'User-Agent': ''
}
}
""".trimIndent()
}
@ -103,4 +106,7 @@ object AppConst {
var versionCode: Long = 0L,
var versionName: String = ""
)
const val authority = BuildConfig.APPLICATION_ID + ".fileProvider"
}

@ -7,11 +7,12 @@ object AppPattern {
val JS_PATTERN: Pattern =
Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
//图片有data-开头的数据属性时优先用数据属性作为src,没有数据属性时才匹配src
val imgPattern: Pattern =
Pattern.compile("<img .*?src.*?=.*?\"(.*?(?:,\\{.*\\})?)\".*?>", Pattern.CASE_INSENSITIVE)
Pattern.compile("<img(?:(?![^>]*data-)[^>]*src|[^>]*data-)[^=]*= *\"([^\"{]+(?:\\{(?:[^{}]|\\{[^{}]*\\})*\\})?)\"[^>]*>", Pattern.CASE_INSENSITIVE)
val nameRegex = Regex("\\s+作\\s*者.*")
val authorRegex = Regex(".*?作\\s*?者[::]")
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
val authorRegex = Regex("^.*?作\\s*者[::\\s]*|\\s+著")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val splitGroupRegex = Regex("[,;,;]")
}
}

@ -16,7 +16,7 @@ object EventBus {
const val AUDIO_PROGRESS = "audioProgress"
const val AUDIO_SIZE = "audioSize"
const val AUDIO_SPEED = "audioSpeed"
const val SHOW_RSS = "showRss"
const val NOTIFY_MAIN = "notifyMain"
const val WEB_SERVICE = "webService"
const val UP_DOWNLOAD = "upDownload"
const val SAVE_CONTENT = "saveContent"

@ -5,6 +5,8 @@ object PreferKey {
const val themeMode = "themeMode"
const val userAgent = "userAgent"
const val showUnread = "showUnread"
const val bookGroupStyle = "bookGroupStyle"
const val useDefaultCover = "useDefaultCover"
const val hideStatusBar = "hideStatusBar"
const val clickActionTL = "clickActionTopLeft"
const val clickActionTC = "clickActionTopCenter"
@ -22,9 +24,12 @@ object PreferKey {
const val ttsSpeechRate = "ttsSpeechRate"
const val prevKeys = "prevKeyCodes"
const val nextKeys = "nextKeyCodes"
const val showDiscovery = "showDiscovery"
const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort"
const val bookExportFileName = "bookExportFileName"
const val bookImportFileName = "bookImportFileName"
const val recordLog = "recordLog"
const val processText = "process_text"
const val cleanCache = "cleanCache"
@ -88,4 +93,4 @@ object PreferKey {
const val cNBBackground = "colorBottomBackgroundNight"
const val bgImageN = "backgroundImageNight"
}
}

@ -24,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class],
version = 32,
version = 33,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -60,7 +60,7 @@ abstract class AppDatabase : RoomDatabase() {
migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25, migration_25_26, migration_26_27,
migration_27_28, migration_28_29, migration_29_30, migration_30_31,
migration_31_32
migration_31_32, migration_32_33
)
.allowMainThreadQueries()
.addCallback(dbCallback)
@ -304,6 +304,33 @@ abstract class AppDatabase : RoomDatabase() {
database.execSQL("DROP TABLE `epubChapters`")
}
}
private val migration_32_33 = object : Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL,
`bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL,
`chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL,
`content` TEXT NOT NULL, PRIMARY KEY(`time`))
"""
)
database.execSQL(
"""
CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`)
"""
)
database.execSQL(
"""
insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content)
select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor,
chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o
left join books b on o.bookUrl = b.bookUrl
"""
)
}
}
}
}

@ -11,15 +11,23 @@ interface BookmarkDao {
@get:Query("select * from bookmarks")
val all: List<Bookmark>
@Query("select * from bookmarks where bookUrl = :bookUrl or (bookName = :bookName and bookAuthor = :bookAuthor)")
@Query("select * from bookmarks where bookName = :bookName and bookAuthor = :bookAuthor")
fun observeByBook(
bookUrl: String,
bookName: String,
bookAuthor: String
): LiveData<List<Bookmark>>
@Query("SELECT * FROM bookmarks where bookUrl = :bookUrl and chapterName like '%'||:key||'%' or content like '%'||:key||'%'")
fun liveDataSearch(bookUrl: String, key: String): LiveData<List<Bookmark>>
@Query(
"""
SELECT * FROM bookmarks where bookName = :bookName and bookAuthor = :bookAuthor
and chapterName like '%'||:key||'%' or content like '%'||:key||'%'
"""
)
fun liveDataSearch(
bookName: String,
bookAuthor: String,
key: String
): LiveData<List<Bookmark>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookmark: Bookmark)
@ -30,7 +38,4 @@ interface BookmarkDao {
@Delete
fun delete(vararg bookmark: Bookmark)
@Query("delete from bookmarks where bookUrl = :bookUrl and chapterName like '%'||:chapterName||'%'")
fun delByBookmark(bookUrl: String, chapterName: String)
}

@ -113,6 +113,12 @@ data class Book(
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
//自定义简介有自动更新的需求时,可通过更新intro再调用upCustomIntro()完成
@Suppress("unused")
fun upCustomIntro() {
customIntro = intro
}
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
@ -124,6 +130,14 @@ data class Book(
return readConfig!!
}
fun setReverseToc(reverseToc: Boolean) {
config().reverseToc = reverseToc
}
fun getReverseToc(): Boolean {
return config().reverseToc
}
fun setUseReplaceRule(useReplaceRule: Boolean) {
config().useReplaceRule = useReplaceRule
}
@ -156,14 +170,6 @@ data class Book(
config().imageStyle = imageStyle
}
fun getDelParagraph(): Int {
return config().delParagraph
}
fun setDelParagraph(num: Int) {
config().delParagraph = num
}
fun setDelTag(tag: Long) {
config().delTag =
if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag
@ -208,17 +214,10 @@ data class Book(
newBook.customTag = customTag
newBook.canUpdate = canUpdate
newBook.readConfig = readConfig
delete()
delete(this)
appDb.bookDao.insert(newBook)
}
fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null
}
appDb.bookDao.delete(this)
}
fun upInfoFromOld(oldBook: Book?) {
oldBook?.let {
group = oldBook.group
@ -234,6 +233,13 @@ data class Book(
}
}
fun createBookMark(): Bookmark {
return Bookmark(
bookName = name,
bookAuthor = author,
)
}
companion object {
const val hTag = 2L
const val rubyTag = 4L
@ -241,15 +247,23 @@ data class Book(
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
data class ReadConfig(
var reverseToc: Boolean = false,
var pageAnim: Int = -1,
var reSegment: Boolean = false,
var imageStyle: String? = null,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则
var delParagraph: Int = 0,//去除段首
var delTag: Long = 0L,//去除标签
) : Parcelable
@ -261,4 +275,4 @@ data class Book(
@TypeConverter
fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json)
}
}
}

@ -7,12 +7,14 @@ import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "bookmarks", indices = [(Index(value = ["time"], unique = true))])
@Entity(
tableName = "bookmarks",
indices = [(Index(value = ["bookName", "bookAuthor"], unique = false))]
)
data class Bookmark(
@PrimaryKey
var time: Long = System.currentTimeMillis(),
var bookUrl: String = "",
var bookName: String = "",
val time: Long = System.currentTimeMillis(),
val bookName: String = "",
val bookAuthor: String = "",
var chapterIndex: Int = 0,
var chapterPos: Int = 0,

@ -78,6 +78,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefBoolean(PreferKey.showUnread, value)
}
val useDefaultCover: Boolean
get() = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false)
val isTransparentStatusBar: Boolean
get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
@ -87,6 +90,24 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val screenOrientation: String?
get() = appCtx.getPrefString(PreferKey.screenOrientation)
var bookGroupStyle: Int
get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0)
set(value) {
appCtx.putPrefInt(PreferKey.bookGroupStyle, value)
}
var bookExportFileName: String?
get() = appCtx.getPrefString(PreferKey.bookExportFileName)
set(value) {
appCtx.putPrefString(PreferKey.bookExportFileName, value)
}
var bookImportFileName: String?
get() = appCtx.getPrefString(PreferKey.bookImportFileName)
set(value) {
appCtx.putPrefString(PreferKey.bookImportFileName, value)
}
var backupPath: String?
get() = appCtx.getPrefString(PreferKey.backupPath)
set(value) {
@ -97,7 +118,10 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
}
}
val isShowRSS: Boolean
val showDiscovery: Boolean
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
val showRSS: Boolean
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
val autoRefreshBook: Boolean

@ -15,7 +15,6 @@ import org.apache.commons.text.similarity.JaccardSimilarity
import splitties.init.appCtx
import java.io.File
import java.util.concurrent.CopyOnWriteArraySet
import java.util.regex.Matcher
import java.util.regex.Pattern
import kotlin.math.abs
import kotlin.math.max
@ -39,7 +38,7 @@ object BookHelp {
}
/**
* 已删除书的缓存
* 已删除书的缓存
*/
fun clearRemovedCache() {
Coroutine.async {
@ -56,14 +55,14 @@ object BookHelp {
}
}
fun getEpubFile(book: Book,): File {
fun getEpubFile(book: Book): File {
val file = FileUtils.getFile(
downloadDir,
cacheFolderName,
book.getFolderName(),
"index.epubx"
)
if(!file.exists()){
if (!file.exists()) {
val input = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl)
appCtx.contentResolver.openInputStream(uri)
@ -327,39 +326,53 @@ object BookHelp {
}
}
private val chapterNamePattern by lazy {
Pattern.compile("^(.*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟0-9\\s]+)[章节篇回集])[、,。 ::.\\s]*")
private val chapterNamePattern1 by lazy {
Pattern.compile(".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集]")
}
private val chapterNamePattern2 by lazy {
Pattern.compile("^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、]|\\.[^\\d])")
}
private val regexA by lazy {
return@lazy "\\s".toRegex()
}
private fun getChapterNum(chapterName: String?): Int {
if (chapterName != null) {
val matcher: Matcher = chapterNamePattern.matcher(chapterName)
if (matcher.find()) {
return StringUtils.stringToInt(matcher.group(2))
}
}
return -1
chapterName ?: return -1
val chapterName1 = StringUtils.fullToHalf(chapterName).replace(regexA, "")
return StringUtils.stringToInt(
(
chapterNamePattern1.matcher(chapterName1).takeIf { it.find() }
?: chapterNamePattern2.matcher(chapterName1).takeIf { it.find() }
)?.group(1)
?:"-1"
)
}
@Suppress("SpellCheckingInspection")
private val regexOther by lazy {
// 所有非字母数字中日韩文字 CJK区+扩展A-F区
@Suppress("RegExpDuplicateCharacterInClass")
return@lazy "[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]".toRegex()
}
private val regexA by lazy {
return@lazy "\\s".toRegex()
}
private val regexB by lazy {
return@lazy "^第.*?章|[(\\[][^()\\[\\]]{2,}[)\\]]$".toRegex()
//章节序号,排除处于结尾的状况,避免将章节名替换为空字串
return@lazy "^.*?第(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话](?!$)|^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、](?!$)|\\.(?=[^\\d]))".toRegex()
}
private val regexC by lazy {
//前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex()
}
private fun getPureChapterName(chapterName: String?): String {
return if (chapterName == null) "" else StringUtils.fullToHalf(chapterName)
.replace(regexA, "")
.replace(regexB, "")
.replace(regexC, "")
.replace(regexOther, "")
}
}
}

@ -346,7 +346,7 @@ interface JsExtensions {
str: String,
key: String,
transformation: String,
iv: String = ""
iv: String
): ByteArray? {
return EncoderUtils.decryptAES(
@ -369,7 +369,7 @@ interface JsExtensions {
str: String,
key: String,
transformation: String,
iv: String = ""
iv: String
): String? {
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
@ -386,7 +386,7 @@ interface JsExtensions {
str: String,
key: String,
transformation: String,
iv: String = ""
iv: String
): ByteArray? {
return EncoderUtils.decryptBase64AES(
data = str.encodeToByteArray(),
@ -408,7 +408,7 @@ interface JsExtensions {
str: String,
key: String,
transformation: String,
iv: String = ""
iv: String
): String? {
return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
@ -422,7 +422,7 @@ interface JsExtensions {
*/
fun aesEncodeToByteArray(
data: String, key: String, transformation: String,
iv: String = ""
iv: String
): ByteArray? {
return EncoderUtils.encryptAES(
data.encodeToByteArray(),
@ -441,7 +441,7 @@ interface JsExtensions {
*/
fun aesEncodeToString(
data: String, key: String, transformation: String,
iv: String = ""
iv: String
): String? {
return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) }
}
@ -455,7 +455,7 @@ interface JsExtensions {
*/
fun aesEncodeToBase64ByteArray(
data: String, key: String, transformation: String,
iv: String = ""
iv: String
): ByteArray? {
return EncoderUtils.encryptAES2Base64(
data.encodeToByteArray(),
@ -474,7 +474,7 @@ interface JsExtensions {
*/
fun aesEncodeToBase64String(
data: String, key: String, transformation: String,
iv: String = ""
iv: String
): String? {
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
}

@ -51,9 +51,9 @@ object MediaHelp {
audioManager: AudioManager,
focusRequest: AudioFocusRequestCompat?
): Boolean {
val request: Int =
focusRequest?.let { AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) }
?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
val request = focusRequest?.let {
AudioManagerCompat.requestAudioFocus(audioManager, focusRequest)
} ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}

@ -1,5 +1,6 @@
package io.legado.app.help
import android.content.Context
import io.legado.app.R
import splitties.init.appCtx
@ -7,12 +8,6 @@ object ReadTipConfig {
val tips by lazy {
appCtx.resources.getStringArray(R.array.read_tip).toList()
}
val headerModes by lazy {
linkedMapOf(0 to "状态栏显示时隐藏", 1 to "显示", 2 to "隐藏")
}
val footerModes by lazy {
linkedMapOf(0 to "显示", 1 to "隐藏")
}
const val none = 0
const val chapterTitle = 1
const val time = 2
@ -83,4 +78,19 @@ object ReadTipConfig {
set(value) {
ReadBookConfig.config.tipColor = value
}
fun getHeaderModes(context: Context): LinkedHashMap<Int, String> {
return linkedMapOf(
Pair(0, context.getString(R.string.hide_when_status_bar_show)),
Pair(1, context.getString(R.string.show)),
Pair(2, context.getString(R.string.hide))
)
}
fun getFooterModes(context: Context): LinkedHashMap<Int, String> {
return linkedMapOf(
Pair(0, context.getString(R.string.show)),
Pair(1, context.getString(R.string.hide))
)
}
}

@ -101,9 +101,9 @@ val Fragment.secondaryDisabledTextColor: Int
val Context.buttonDisabledColor: Int
get() = if (isDarkTheme) {
ContextCompat.getColor(this, R.color.ate_button_disabled_dark)
ContextCompat.getColor(this, R.color.md_dark_disabled)
} else {
ContextCompat.getColor(this, R.color.ate_button_disabled_light)
ContextCompat.getColor(this, R.color.md_light_disabled)
}
val Context.isDarkTheme: Boolean

@ -7,6 +7,7 @@ import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.intellij.lang.annotations.Language
import org.jsoup.Jsoup
import java.io.File
import java.io.InputStream
@ -19,6 +20,7 @@ import java.util.*
class WebDav(urlStr: String) {
companion object {
// 指定返回哪些属性
@Language("xml")
private const val DIR =
"""<?xml version="1.0"?>
<a:propfind xmlns:a="DAV:">

@ -187,7 +187,7 @@ object Debug {
private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
if (book.tocUrl.isNotBlank()) {
log(debugSource, "目录url不为空,详情页已解析")
log(debugSource, "≡已获取目录链接,跳过详情页")
log(debugSource, showTime = false)
tocDebug(scope, webBook, book)
return

@ -650,9 +650,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
bindings["title"] = chapter?.title
bindings["src"] = content
bindings["nextChapterUrl"] = nextChapterUrl
return runBlocking {
SCRIPT_ENGINE.eval(jsStr, bindings)
}
return SCRIPT_ENGINE.eval(jsStr, bindings)
}
/**

@ -2,15 +2,19 @@ package io.legado.app.model.localBook
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.script.SimpleBindings
object LocalBook {
private const val folderName = "bookTxt"
@ -39,7 +43,7 @@ object LocalBook {
fun importFile(uri: Uri): Book {
val path: String
val fileName = if (uri.isContentScheme()) {
val fileName = (if (uri.isContentScheme()) {
path = uri.toString()
val doc = DocumentFile.fromSingleUri(appCtx, uri)
doc?.let {
@ -55,67 +59,54 @@ object LocalBook {
} else {
path = uri.path!!
File(path).name
}
}).replace(Regex("\\.txt$"), "")
val name: String
val author: String
if (("" in fileName && "" in fileName)
|| "作者" in fileName
|| (fileName.contains(" by ", true))
) {
//匹配(知轩藏书常用格式) 《书名》其它信息作者:作者名.txt
val m1 = Pattern
.compile("《(.*?)》.*?作者:(.*?)\\.txt")
.matcher(fileName)
//匹配 书名 by 作者名.txt
val m2 = Pattern
.compile("txt\\.(.*?) yb (.*?)$")
.matcher(fileName.reversed())
if (m1.find()) {
name = m1.group(1) ?: fileName.replace(".txt", "")
author = m1.group(2) ?: ""
BookHelp.formatBookAuthor(author)
} else if (m2.find()) {
var temp = m2.group(2)
name = if (temp==null||temp == "") {
fileName.replace(".txt", "")
} else {
temp.reversed()
}
temp = m2.group(1) ?: ""
author = temp.reversed()
BookHelp.formatBookAuthor(author)
} else {
//匹配(知轩藏书常用格式) 《书名》其它信息作者:作者名.txt
val m1 = Pattern
.compile("(.*?)《([^《》]+)》(.*)")
.matcher(fileName)
val st = fileName.indexOf("")
val e = fileName.indexOf("")
name = if (e > st && st != -1) {
fileName.substring(st + 1, e)
} else {
fileName.replace(".txt", "")
}
//匹配 书名 by 作者名.txt
val m2 = Pattern
.compile("(^)(.+) by (.+)$")
.matcher(fileName)
(m1.takeIf { m1.find() } ?: m2.takeIf { m2.find() }).run {
val s = fileName.indexOf("作者")
author = if (s != -1 && s + 2 < fileName.length) {
fileName.substring(s + 2).replace(".txt", "")
} else {
""
}
BookHelp.formatBookAuthor(author)
if (this is Matcher) {
//按默认格式将文件名分解成书名、作者名
name = group(2)!!
author = BookHelp.formatBookAuthor((group(1) ?: "") + (group(3) ?: ""))
} else if (!AppConfig.bookImportFileName.isNullOrBlank()) {
//在脚本中定义如何分解文件名成书名、作者名
val jsonStr = AppConst.SCRIPT_ENGINE.eval(
//在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获
AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})",
//将文件名注入到脚步的src变量中
SimpleBindings().also{ it["src"] = fileName }
).toString()
val bookMess = GSON.fromJsonObject<HashMap<String, String>>(jsonStr) ?: HashMap()
name = bookMess["name"] ?: fileName
author = bookMess["author"]?.takeIf { it.length != fileName.length } ?: ""
} else {
name = fileName.replace(AppPattern.nameRegex, "")
author = fileName.replace(AppPattern.authorRegex, "")
.takeIf { it.length != fileName.length } ?: ""
}
} else {
name = fileName.replace(".txt", "")
author = ""
}
val book = Book(
bookUrl = path,
name = name,
@ -138,10 +129,10 @@ object LocalBook {
val bookFile = FileUtils.getFile(cacheFolder, book.originName)
bookFile.delete()
}
if(book.isEpub()){
val bookFile=BookHelp.getEpubFile(book).parentFile
if (bookFile!=null&&bookFile.exists()){
FileUtils.delete(bookFile,true)
if (book.isEpub()) {
val bookFile = BookHelp.getEpubFile(book).parentFile
if (bookFile != null && bookFile.exists()) {
FileUtils.delete(bookFile, true)
}
}
@ -156,4 +147,4 @@ object LocalBook {
}
}
}
}
}

@ -107,10 +107,12 @@ object BookChapterList {
}
val lh = LinkedHashSet(chapterList)
val list = ArrayList(lh)
list.reverse()
if (!book.getReverseToc()) {
list.reverse()
}
Debug.log(book.origin, "◇目录总数:${list.size}")
for ((index, item) in list.withIndex()) {
item.index = index
list.forEachIndexed { index, bookChapter ->
bookChapter.index = index
}
book.latestChapterTitle = list.last().title
book.durChapterTitle =
@ -188,12 +190,12 @@ object BookChapterList {
chapterList.add(bookChapter)
}
}
Debug.log(bookSource.bookSourceUrl, "解析目录列表完成", log)
Debug.log(bookSource.bookSourceUrl, "┌首章名称", log)
Debug.log(bookSource.bookSourceUrl, "└目录列表解析完成", log)
Debug.log(bookSource.bookSourceUrl, "获取首章名称", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log)
Debug.log(bookSource.bookSourceUrl, "┌首章链接", log)
Debug.log(bookSource.bookSourceUrl, "获取首章链接", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log)
Debug.log(bookSource.bookSourceUrl, "┌首章信息", log)
Debug.log(bookSource.bookSourceUrl, "获取首章信息", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log)
}
return ChapterData(chapterList, nextUrlList)

@ -26,6 +26,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
abstract class BaseReadAloudService : BaseService(),
@ -202,7 +203,11 @@ abstract class BaseReadAloudService : BaseService(),
* @return 音频焦点
*/
fun requestFocus(): Boolean {
return MediaHelp.requestFocus(audioManager, mFocusRequest)
val requestFocus = MediaHelp.requestFocus(audioManager, mFocusRequest)
if (!requestFocus) {
toastOnUi("未获取到音频焦点")
}
return requestFocus
}
/**

@ -12,7 +12,6 @@ import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
@ -133,14 +132,9 @@ class DownloadService : BaseService() {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0版本以上
val uriForFile: Uri =
FileProvider.getUriForFile(
this,
"${BuildConfig.APPLICATION_ID}.fileProvider",
file
)
val contentUrl = FileProvider.getUriForFile(this, AppConst.authority, file)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(uriForFile, "application/vnd.android.package-archive")
intent.setDataAndType(contentUrl, "application/vnd.android.package-archive")
} else {
val uri: Uri = Uri.fromFile(file)
intent.setDataAndType(uri, "application/vnd.android.package-archive")

@ -20,10 +20,12 @@ class AboutFragment : PreferenceFragmentCompat() {
private val licenseUrl = "https://github.com/gedoor/legado/blob/master/LICENSE"
private val disclaimerUrl = "https://gedoor.github.io/MyBookshelf/disclaimer.html"
private val qqGroups = linkedMapOf(
Pair("(QQ群VIP中转)1017837876", "0d9-zpmqbYfK3i_wt8uCvQoB2lmXadrg"),
Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"),
Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"),
Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"),
Pair("(QQ群VIP4)682555679", "VF2UwvUCuaqlo6pddWTe_kw__a1_Fr8O"),
Pair("(QQ群VIP5)161622578", "S81xdnhJ5EBC389LTUvoyiyM-wr71pvJ"),
Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"),
Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"),
Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"),
@ -32,7 +34,8 @@ class AboutFragment : PreferenceFragmentCompat() {
Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n"),
Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC"),
Pair("(QQ群8)1079926194", "gg2qFH8q9IPFaCHV3H7CqCN-YljvazE1"),
Pair("(QQ群9)892108780", "Ci_O3aysKjEBfplOWeCud-rxl71TjU2Q")
Pair("(QQ群9)892108780", "Ci_O3aysKjEBfplOWeCud-rxl71TjU2Q"),
Pair("(QQ群10)812720266", "oW9ksY0sAWUEq0hfM5irN5aOdvKVgMEE")
)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

@ -14,7 +14,10 @@ import io.legado.app.data.entities.ReadRecordShow
import io.legado.app.databinding.ActivityReadRecordBinding
import io.legado.app.databinding.ItemReadRecordBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.utils.cnCompare
import io.legado.app.utils.startActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
@ -111,6 +114,23 @@ class ReadRecordActivity : BaseActivity<ActivityReadRecordBinding>() {
override fun registerListener(holder: ItemViewHolder, binding: ItemReadRecordBinding) {
binding.apply {
root.setOnClickListener {
val item = getItem(holder.layoutPosition) ?: return@setOnClickListener
launch {
val book = withContext(IO) {
appDb.bookDao.findByName(item.bookName).firstOrNull()
}
if (book == null) {
startActivity<SearchActivity> {
putExtra("key", item.bookName)
}
} else {
startActivity<ReadBookActivity> {
putExtra("bookUrl", book.bookUrl)
}
}
}
}
tvRemove.setOnClickListener {
getItem(holder.layoutPosition)?.let { item ->
sureDelAlert(item)

@ -26,6 +26,9 @@ import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.cnCompare
import io.legado.app.utils.getPrefInt
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBookViewModel>(),
@ -50,7 +53,12 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
override fun onActivityCreated(savedInstanceState: Bundle?) {
groupId = intent.getLongExtra("groupId", -1)
binding.titleBar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all)
launch {
binding.titleBar.subtitle = withContext(IO) {
appDb.bookGroupDao.getByID(groupId)?.groupName
?: getString(R.string.no_group)
}
}
initView()
initGroupData()
initBookData()

@ -67,7 +67,12 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override fun onActivityCreated(savedInstanceState: Bundle?) {
groupId = intent.getLongExtra("groupId", -1)
binding.titleBar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all)
launch {
binding.titleBar.subtitle = withContext(IO) {
appDb.bookGroupDao.getByID(groupId)?.groupName
?: getString(R.string.no_group)
}
}
initRecyclerView()
initGroupData()
initBookData()
@ -122,6 +127,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
exportPosition = -1
selectExportFolder()
}
R.id.menu_export_file_name -> alertExportFileName()
R.id.menu_export_type -> showExportTypeConfig()
R.id.menu_export_charset -> showCharsetConfig()
R.id.menu_log ->
@ -289,6 +295,19 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}
}
private fun alertExportFileName() {
alert(R.string.export_file_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setText(AppConfig.bookExportFileName)
}
customView { alertBinding.root }
okButton {
AppConfig.bookExportFileName = alertBinding.editView.text?.toString()
}
cancelButton()
}.show()
}
private fun showExportTypeConfig() {
selector(R.string.export_type, arrayListOf("txt", "epub")) { _, i ->
AppConfig.exportType = i

@ -10,6 +10,7 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@ -22,14 +23,26 @@ import io.legado.app.utils.*
import me.ag2s.epublib.domain.*
import me.ag2s.epublib.epub.EpubWriter
import me.ag2s.epublib.util.ResourceUtil
import splitties.init.appCtx
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.Charset
import javax.script.SimpleBindings
class CacheViewModel(application: Application) : BaseViewModel(application) {
fun getExportFileName(book: Book): String {
val jsStr = AppConfig.bookExportFileName
if (jsStr.isNullOrBlank()) {
return "${book.name} 作者:${book.getRealAuthor()}"
}
val bindings = SimpleBindings()
bindings["name"] = book.name
bindings["author"] = book.getRealAuthor()
return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString()
}
fun export(path: String, book: Book, finally: (msg: String) -> Unit) {
execute {
@ -50,7 +63,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun export(doc: DocumentFile, book: Book) {
val filename = "${book.name} by ${book.author}.txt"
val filename = "${getExportFileName(book)}.txt"
DocumentUtils.delete(doc, filename)
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
val stringBuilder = StringBuilder()
@ -80,7 +93,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
}
private suspend fun export(file: File, book: Book) {
val filename = "${book.name} by ${book.author}.txt"
val filename = "${getExportFileName(book)}.txt"
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
val stringBuilder = StringBuilder()
@ -110,11 +123,29 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
private fun getAllContents(book: Book, append: (text: String) -> Unit) {
val useReplace = AppConfig.exportUseReplace
val contentProcessor = ContentProcessor(book.name, book.origin)
append("${book.name}\n${context.getString(R.string.author_show, book.author)}")
append(
"${book.name}\n${
context.getString(
R.string.author_show,
book.getRealAuthor()
)
}\n${
context.getString(
R.string.intro_show,
"\n" + HtmlFormatter.format(book.getDisplayIntro())
)
}"
)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter).let { content ->
val content1 = contentProcessor
.getContent(book, chapter.title, content ?: "null", false, useReplace)
.getContent(
book,
chapter.title.replace("\\r?\\n".toRegex(), " "),
content ?: "null",
false,
useReplace
)
.joinToString("\n")
append.invoke("\n\n$content1")
}
@ -126,9 +157,9 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter)?.let { content ->
content.split("\n").forEachIndexed { index, text ->
val matches = AppPattern.imgPattern.toRegex().findAll(input = text)
matches.forEach { matchResult ->
matchResult.groupValues[1].let {
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) {
matcher.group(1)?.let {
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
srcList.add(Triple(chapter.title, index, src))
}
@ -161,7 +192,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
@Suppress("BlockingMethodInNonBlockingContext")
private fun exportEpub(doc: DocumentFile, book: Book) {
val filename = "${book.name} by ${book.author}.epub"
val filename = "${getExportFileName(book)}.epub"
DocumentUtils.delete(doc, filename)
val epubBook = EpubBook()
epubBook.version = "2.0"
@ -169,12 +200,11 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
setEpubMetadata(book, epubBook)
//set cover
setCover(book, epubBook)
//set css
setCSS(epubBook)
//设置正文
setEpubContent(book, epubBook)
val contentModel = setAssets(doc, book, epubBook)
//设置正文
setEpubContent(contentModel, book, epubBook)
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
EpubWriter().write(epubBook, bookOs)
@ -185,7 +215,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
private fun exportEpub(file: File, book: Book) {
val filename = "${book.name} by ${book.author}.epub"
val filename = "${getExportFileName(book)}.epub"
val epubBook = EpubBook()
epubBook.version = "2.0"
//set metadata
@ -193,28 +223,144 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
//set cover
setCover(book, epubBook)
//set css
setCSS(epubBook)
val contentModel = setAssets(book, epubBook)
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
//设置正文
setEpubContent(book, epubBook)
setEpubContent(contentModel, book, epubBook)
EpubWriter().write(epubBook, FileOutputStream(bookFile))
}
private fun setCSS(epubBook: EpubBook) {
//set css
private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String {
if(!AppConfig.isGooglePlay) return setAssets(book, epubBook)
var contentModel = ""
DocumentUtils.getDirDocument(doc, "Asset").let { customPath ->
if (customPath == null) {//使用内置模板
contentModel = setAssets(book, epubBook)
} else {//外部模板
customPath.listFiles().forEach { folder ->
if (folder.isDirectory && folder.name == "Text") {
folder.listFiles().sortedWith { o1, o2 ->
val name1 = o1.name ?: ""
val name2 = o2.name ?: ""
name1.cnCompare(name2)
}.forEach { file ->
if (file.isFile) {
when {
//正文模板
file.name.equals(
"chapter.html",
true
) || file.name.equals("chapter.xhtml", true) -> {
contentModel = file.readText(context) ?: ""
}
//封面等其他模板
true == file.name?.endsWith("html", true) -> {
epubBook.addSection(
FileUtils.getNameExcludeExtension(
file.name ?: "Cover.html"
),
ResourceUtil.createPublicResource(
book.name,
book.getRealAuthor(),
book.getDisplayIntro(),
book.kind,
book.wordCount,
file.readText(context) ?: "",
"${folder.name}/${file.name}"
)
)
}
else -> {
//其他格式文件当做资源文件
folder.listFiles().forEach {
if (it.isFile)
epubBook.resources.add(
Resource(
it.readBytes(context),
"${folder.name}/${it.name}"
)
)
}
}
}
}
}
} else if (folder.isDirectory) {
//资源文件
folder.listFiles().forEach {
if (it.isFile)
epubBook.resources.add(
Resource(
it.readBytes(context),
"${folder.name}/${it.name}"
)
)
}
} else {//Asset下面的资源文件
epubBook.resources.add(
Resource(
folder.readBytes(context),
"${folder.name}"
)
)
}
}
}
}
return contentModel
}
private fun setAssets(book: Book, epubBook: EpubBook): String {
epubBook.resources.add(
Resource(
appCtx.assets.open("epub/fonts.css").readBytes(),
"Styles/fonts.css"
)
)
epubBook.resources.add(
Resource(
"body,div{background:white;outline:none;width:100%;}h2{color:#005a9c;text-align:left;}p{text-indent:2em;text-align:justify;}img{display:inline-block;width:100%;height:auto;max-width: 100%;max-height:100%;}".encodeToByteArray(),
"css/style.css"
appCtx.assets.open("epub/main.css").readBytes(),
"Styles/main.css"
)
)
epubBook.resources.add(
Resource(
appCtx.assets.open("epub/logo.png").readBytes(),
"Images/logo.png"
)
)
epubBook.addSection(
context.getString(R.string.img_cover),
ResourceUtil.createPublicResource(
book.name,
book.getRealAuthor(),
book.getDisplayIntro(),
book.kind,
book.wordCount,
String(appCtx.assets.open("epub/cover.html").readBytes()),
"Text/cover.html"
)
)
epubBook.addSection(
context.getString(R.string.book_intro),
ResourceUtil.createPublicResource(
book.name,
book.getRealAuthor(),
book.getDisplayIntro(),
book.kind,
book.wordCount,
String(appCtx.assets.open("epub/intro.html").readBytes()),
"Text/intro.html"
)
)
return String(appCtx.assets.open("epub/chapter.html").readBytes())
}
private fun setCover(book: Book, epubBook: EpubBook) {
Glide.with(context)
.asBitmap()
.load(book.getDisplayCover())
@ -225,21 +371,19 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val byteArray: ByteArray = stream.toByteArray()
resource.recycle()
stream.close()
epubBook.coverImage = Resource(byteArray, "cover.jpg")
epubBook.coverImage = Resource(byteArray, "Images/cover.jpg")
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
}
private fun setEpubContent(book: Book, epubBook: EpubBook) {
private fun setEpubContent(contentModel: String, book: Book, epubBook: EpubBook) {
//正文
val useReplace = AppConfig.exportUseReplace
val contentProcessor = ContentProcessor(book.name, book.origin)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
BookHelp.getContent(book, chapter).let { content ->
var content1 = fixPic(epubBook, book, content ?: "null", chapter)
content1 = contentProcessor
@ -247,23 +391,17 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
.joinToString("\n")
epubBook.addSection(
chapter.title,
ResourceUtil.createHTMLResource(chapter.title, content1)
ResourceUtil.createChapterResource(
chapter.title.replace("\uD83D\uDD12", ""),
content1,
contentModel,
"Text/chapter_${index}.html"
)
)
}
}
}
private fun setPic(src: String, book: Book, epubBook: EpubBook) {
val href = "${MD5Utils.md5Encode16(src)}${BookHelp.getImageSuffix(src)}"
val vFile = BookHelp.getImage(book, src)
val fp = FileResourceProvider(vFile.parent)
if (vFile.exists()) {
val img = LazyResource(fp, href)
epubBook.resources.add(img)
}
}
private fun fixPic(
epubBook: EpubBook,
book: Book,
@ -273,19 +411,21 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val data = StringBuilder("")
content.split("\n").forEach { text ->
var text1 = text
val matches = AppPattern.imgPattern.toRegex().findAll(input = text)
matches.forEach { matchResult ->
matchResult.groupValues[1].let {
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) {
matcher.group(1)?.let {
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
setPic(src, book, epubBook)
text1 = text1.replace(
src,
"${MD5Utils.md5Encode16(src)}${BookHelp.getImageSuffix(src)}"
)
val originalHref = "${MD5Utils.md5Encode16(src)}${BookHelp.getImageSuffix(src)}"
val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
val vFile = BookHelp.getImage(book, src)
val fp = FileResourceProvider(vFile.parent)
if (vFile.exists()) {
val img = LazyResource(fp, href, originalHref)
epubBook.resources.add(img)
}
text1 = text1.replace(src, "../${href}")
}
}
data.append(text1).append("\n")
}
return data.toString()
@ -297,13 +437,11 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者
metadata.language = "zh"//数据的语言
metadata.dates.add(Date())//数据的创建日期
metadata.publishers.add("Legado APP")//数据的创建者
metadata.publishers.add("Legado")//数据的创建者
metadata.descriptions.add(book.getDisplayIntro())//书籍的简介
//metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍
epubBook.metadata = metadata
}
//////end of EPUB
}

@ -17,12 +17,12 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.BookType
import io.legado.app.constant.Theme
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.databinding.ActivityBookInfoBinding
import io.legado.app.help.BlurTransformation
import io.legado.app.help.ImageLoader
import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
@ -38,6 +38,9 @@ import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class BookInfoActivity :
@ -50,11 +53,19 @@ class BookInfoActivity :
private val tocActivityResult = registerForActivityResult(TocActivityResult()) {
it?.let {
viewModel.bookData.value?.let { book ->
if (book.durChapterIndex != it.first) {
book.durChapterIndex = it.first
book.durChapterPos = it.second
launch {
withContext(IO) {
viewModel.durChapterIndex = it.first
book.durChapterIndex = it.first
book.durChapterPos = it.second
appDb.bookDao.update(book)
}
viewModel.chapterListData.value?.let { chapterList ->
binding.tvToc.text =
getString(R.string.toc_s, chapterList[book.durChapterIndex].title)
}
startReadActivity(book)
}
startReadActivity(book)
}
} ?: let {
if (!viewModel.inBookshelf) {
@ -65,6 +76,7 @@ class BookInfoActivity :
private val readBookResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
viewModel.refreshData(intent)
if (it.resultCode == RESULT_OK) {
viewModel.inBookshelf = true
upTvBookshelf()
@ -105,6 +117,14 @@ class BookInfoActivity :
return super.onCompatCreateOptionsMenu(menu)
}
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
menu.findItem(R.id.menu_can_update)?.isChecked =
viewModel.bookData.value?.canUpdate ?: true
menu.findItem(R.id.menu_login)?.isVisible =
!viewModel.bookSource?.loginUrl.isNullOrBlank()
return super.onMenuOpened(featureId, menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_edit -> {
@ -156,12 +176,6 @@ class BookInfoActivity :
return super.onCompatOptionsItemSelected(item)
}
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
menu.findItem(R.id.menu_can_update)?.isChecked =
viewModel.bookData.value?.canUpdate ?: true
return super.onMenuOpened(featureId, menu)
}
private fun showBook(book: Book) = with(binding) {
showCover(book)
tvName.text = book.name
@ -359,7 +373,6 @@ class BookInfoActivity :
Intent(this, ReadBookActivity::class.java)
.putExtra("bookUrl", book.bookUrl)
.putExtra("inBookshelf", viewModel.inBookshelf)
.putExtra("key", IntentDataHelp.putData(book))
)
}
}

@ -8,6 +8,7 @@ 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.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.help.BookHelp
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
@ -19,6 +20,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
val chapterListData = MutableLiveData<List<BookChapter>>()
var durChapterIndex = 0
var inBookshelf = false
var bookSource: BookSource? = null
fun initData(intent: Intent) {
execute {
@ -33,9 +35,20 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
fun refreshData(intent: Intent) {
execute {
val name = intent.getStringExtra("name") ?: ""
val author = intent.getStringExtra("author") ?: ""
appDb.bookDao.getBook(name, author)?.let { book ->
setBook(book)
}
}
}
private fun setBook(book: Book) {
durChapterIndex = book.durChapterIndex
bookData.postValue(book)
initBookSource(book)
if (book.tocUrl.isEmpty()) {
loadBookInfo(book)
} else {
@ -48,6 +61,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
private fun initBookSource(book: Book) {
bookSource = if (book.isLocalBook()) {
null
} else {
appDb.bookSourceDao.getBookSource(book.origin)
}
}
fun loadBookInfo(
book: Book, canReName: Boolean = true,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
@ -56,7 +77,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
if (book.isLocalBook()) {
loadChapter(book, changeDruChapterIndex)
} else {
appDb.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
bookSource?.let { bookSource ->
WebBook(bookSource).getBookInfo(this, book, canReName = canReName)
.onSuccess(IO) {
bookData.postValue(book)
@ -87,7 +108,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
chapterListData.postValue(it)
}
} else {
appDb.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
bookSource?.let { bookSource ->
WebBook(bookSource).getChapterList(this, book)
.onSuccess(IO) {
if (it.isNotEmpty()) {
@ -135,6 +156,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
bookData.postValue(newBook)
initBookSource(newBook)
if (newBook.tocUrl.isEmpty()) {
loadBookInfo(newBook, false) {
upChangeDurChapterIndex(newBook, oldTocSize, it)
@ -223,7 +245,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
execute {
bookData.value?.let {
it.delete()
Book.delete(it)
inBookshelf = false
if (it.isLocalBook()) {
LocalBook.deleteBook(it, deleteOriginal)

@ -15,8 +15,10 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.appDb
import io.legado.app.databinding.ActivityImportBookBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.document.FilePicker
@ -78,6 +80,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
when (item.itemId) {
R.id.menu_select_folder -> selectFolder.launch(null)
R.id.menu_scan_folder -> scanFolder()
R.id.menu_import_file_name -> alertImportFileName()
}
return super.onCompatOptionsItemSelected(item)
}
@ -275,6 +278,19 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
}
}
private fun alertImportFileName() {
alert(R.string.import_file_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setText(AppConfig.bookImportFileName)
}
customView { alertBinding.root }
okButton {
AppConfig.bookImportFileName = alertBinding.editView.text?.toString()
}
cancelButton()
}.show()
}
private val find: (docItem: DocItem) -> Unit = {
launch {
adapter.addItem(it)
@ -322,4 +338,4 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
binding.selectActionBar.upCountView(adapter.selectedUris.size, adapter.checkableCount)
}
}
}

@ -23,7 +23,6 @@ import io.legado.app.constant.Status
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.Bookmark
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig
import io.legado.app.help.storage.Backup
@ -47,7 +46,7 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.book.searchContent.SearchContentActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.dialog.TextDialog
@ -73,9 +72,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
private val tocActivity =
registerForActivityResult(TocActivityResult()) {
it?.let {
if (it.first != ReadBook.durChapterIndex) {
viewModel.openChapter(it.first, it.second)
}
viewModel.openChapter(it.first, it.second)
}
}
private val sourceEditActivity =
@ -243,14 +240,12 @@ class ReadBookActivity : ReadBookBaseActivity(),
val book = ReadBook.book
val page = ReadBook.curTextChapter?.page(ReadBook.durPageIndex())
if (book != null && page != null) {
val bookmark = Bookmark(
bookUrl = book.bookUrl,
bookName = book.name,
chapterIndex = ReadBook.durChapterIndex,
chapterPos = ReadBook.durChapterPos,
chapterName = page.title,
val bookmark = book.createBookMark().apply {
chapterIndex = ReadBook.durChapterIndex
chapterPos = ReadBook.durChapterPos
chapterName = page.title
bookText = page.text.trim()
)
}
showBookMark(bookmark)
}
}
@ -773,7 +768,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
override fun showLogin() {
ReadBook.webBook?.bookSource?.let {
startActivity<SourceLogin> {
startActivity<SourceLoginActivity> {
putExtra("sourceUrl", it.bookSourceUrl)
putExtra("loginUrl", it.loginUrl)
putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME])

@ -10,7 +10,6 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.storage.BookWebDav
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.PreciseSearch
@ -30,9 +29,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun initData(intent: Intent) {
execute {
ReadBook.inBookshelf = intent.getBooleanExtra("inBookshelf", true)
IntentDataHelp.getData<Book>(intent.getStringExtra("key"))?.let {
initBook(it)
} ?: intent.getStringExtra("bookUrl")?.let {
intent.getStringExtra("bookUrl")?.let {
appDb.bookDao.getBook(it)?.let { book ->
initBook(book)
}
@ -40,9 +37,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
initBook(it)
}
}.onFinally {
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
ReadBook.saveRead()
}
}
@ -73,9 +68,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (ReadBook.durChapterIndex != book.durChapterIndex) {
ReadBook.durChapterIndex = book.durChapterIndex
ReadBook.durChapterPos = book.durChapterPos
ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null
ReadBook.nextTextChapter = null
ReadBook.clearTextChapter()
}
ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book)
@ -251,9 +244,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null
ReadBook.nextTextChapter = null
ReadBook.clearTextChapter()
ReadBook.callBack?.upContent()
if (index != ReadBook.durChapterIndex) {
ReadBook.durChapterIndex = index
@ -267,7 +258,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun removeFromBookshelf(success: (() -> Unit)?) {
execute {
ReadBook.book?.delete()
Book.delete(ReadBook.book)
}.onSuccess {
success?.invoke()
}

@ -10,6 +10,7 @@ import io.legado.app.lib.theme.backgroundColor
import io.legado.app.utils.getPrefString
import io.legado.app.utils.hideSoftInput
import io.legado.app.utils.putPrefString
import splitties.views.onClick
class PageKeyDialog(context: Context) : Dialog(context, R.style.AppTheme_AlertDialog) {
@ -18,13 +19,19 @@ class PageKeyDialog(context: Context) : Dialog(context, R.style.AppTheme_AlertDi
init {
setContentView(binding.root)
binding.contentView.setBackgroundColor(context.backgroundColor)
binding.etPrev.setText(context.getPrefString(PreferKey.prevKeys))
binding.etNext.setText(context.getPrefString(PreferKey.nextKeys))
binding.tvOk.setOnClickListener {
context.putPrefString(PreferKey.prevKeys, binding.etPrev.text?.toString())
context.putPrefString(PreferKey.nextKeys, binding.etNext.text?.toString())
dismiss()
binding.run {
contentView.setBackgroundColor(context.backgroundColor)
etPrev.setText(context.getPrefString(PreferKey.prevKeys))
etNext.setText(context.getPrefString(PreferKey.nextKeys))
tvReset.onClick {
etPrev.setText("")
etNext.setText("")
}
tvOk.setOnClickListener {
context.putPrefString(PreferKey.prevKeys, etPrev.text?.toString())
context.putPrefString(PreferKey.nextKeys, etNext.text?.toString())
dismiss()
}
}
}

@ -52,8 +52,8 @@ class TipConfigDialog : BaseDialogFragment() {
dsbTitleTop.progress = ReadBookConfig.titleTopSpacing
dsbTitleBottom.progress = ReadBookConfig.titleBottomSpacing
tvHeaderShow.text = ReadTipConfig.headerModes[ReadTipConfig.headerMode]
tvFooterShow.text = ReadTipConfig.footerModes[ReadTipConfig.footerMode]
tvHeaderShow.text = ReadTipConfig.getHeaderModes(requireContext())[ReadTipConfig.headerMode]
tvFooterShow.text = ReadTipConfig.getFooterModes(requireContext())[ReadTipConfig.footerMode]
tvHeaderLeft.text = ReadTipConfig.tipHeaderLeftStr
tvHeaderMiddle.text = ReadTipConfig.tipHeaderMiddleStr
@ -92,16 +92,18 @@ class TipConfigDialog : BaseDialogFragment() {
postEvent(EventBus.UP_CONFIG, true)
}
llHeaderShow.setOnClickListener {
selector(items = ReadTipConfig.headerModes.values.toList()) { _, i ->
ReadTipConfig.headerMode = ReadTipConfig.headerModes.keys.toList()[i]
tvHeaderShow.text = ReadTipConfig.headerModes[ReadTipConfig.headerMode]
val headerModes = ReadTipConfig.getHeaderModes(requireContext())
selector(items = headerModes.values.toList()) { _, i ->
ReadTipConfig.headerMode = headerModes.keys.toList()[i]
tvHeaderShow.text = headerModes[ReadTipConfig.headerMode]
postEvent(EventBus.UP_CONFIG, true)
}
}
llFooterShow.setOnClickListener {
selector(items = ReadTipConfig.footerModes.values.toList()) { _, i ->
ReadTipConfig.footerMode = ReadTipConfig.footerModes.keys.toList()[i]
tvFooterShow.text = ReadTipConfig.footerModes[ReadTipConfig.footerMode]
val footerModes = ReadTipConfig.getFooterModes(requireContext())
selector(items = footerModes.values.toList()) { _, i ->
ReadTipConfig.footerMode = footerModes.keys.toList()[i]
tvFooterShow.text = footerModes[ReadTipConfig.footerMode]
postEvent(EventBus.UP_CONFIG, true)
}
}

@ -497,17 +497,14 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
fun createBookmark(): Bookmark? {
val page = relativePage(selectStart[0])
page.getTextChapter()?.let { chapter ->
val chapterPos = chapter.getReadLength(page.index) +
page.getSelectStartLength(selectStart[1], selectStart[2])
ReadBook.book?.let { book ->
return Bookmark(
bookUrl = book.bookUrl,
bookName = book.name,
chapterIndex = page.chapterIndex,
chapterPos = chapterPos,
chapterName = chapter.title,
return book.createBookMark().apply {
chapterIndex = page.chapterIndex
chapterPos = chapter.getReadLength(page.index) +
page.getSelectStartLength(selectStart[1], selectStart[2])
chapterName = chapter.title
bookText = selectedText
)
}
}
}
return null

@ -118,12 +118,10 @@ object ChapterProvider {
val matcher = AppPattern.imgPattern.matcher(text)
if (matcher.find()) {
matcher.group(1)?.let { src ->
//if (!book.isEpub()) {
durY = setTypeImage(
book, bookChapter, src,
durY, textPages, book.getImageStyle()
)
//}
durY = setTypeImage(
book, bookChapter, src,
durY, textPages, book.getImageStyle()
)
}
} else {
val isTitle = index == 0
@ -474,7 +472,7 @@ object ChapterProvider {
ReadBookConfig.textFont = ""
ReadBookConfig.save()
Typeface.SANS_SERIF
}
} ?: Typeface.DEFAULT
}
private fun getPaint(typeface: Typeface): Pair<TextPaint, TextPaint> {

@ -83,23 +83,37 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
@Synchronized
private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>, precision: Boolean) {
if (newDataS.isNotEmpty()) {
val prevData = ArrayList(searchBooks)
val precisionData = arrayListOf<SearchBook>()
prevData.forEach {
val copyData = ArrayList(searchBooks)
val equalData = arrayListOf<SearchBook>()
val containsData = arrayListOf<SearchBook>()
val otherData = arrayListOf<SearchBook>()
copyData.forEach {
if (!scope.isActive) return
if (it.name == searchKey || it.author == searchKey) {
precisionData.add(it)
equalData.add(it)
} else if (it.name.contains(searchKey) || it.author.contains(searchKey)) {
containsData.add(it)
} else {
otherData.add(it)
}
}
repeat(precisionData.size) {
if (!scope.isActive) return
prevData.removeAt(0)
}
newDataS.forEach { nBook ->
if (!scope.isActive) return
if (nBook.name == searchKey || nBook.author == searchKey) {
var hasSame = false
precisionData.forEach { pBook ->
equalData.forEach { pBook ->
if (!scope.isActive) return
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
hasSame = true
}
}
if (!hasSame) {
equalData.add(nBook)
}
} else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) {
var hasSame = false
containsData.forEach { pBook ->
if (!scope.isActive) return
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
@ -107,11 +121,11 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
}
}
if (!hasSame) {
precisionData.add(nBook)
containsData.add(nBook)
}
} else if (!precision) {
var hasSame = false
prevData.forEach { pBook ->
otherData.forEach { pBook ->
if (!scope.isActive) return
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
@ -119,17 +133,17 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
}
}
if (!hasSame) {
prevData.add(nBook)
otherData.add(nBook)
}
}
}
if (!scope.isActive) return
precisionData.sortByDescending { it.origins.size }
if (!scope.isActive) return
equalData.sortByDescending { it.origins.size }
equalData.addAll(containsData.sortedByDescending { it.origins.size })
if (!precision) {
precisionData.addAll(prevData)
equalData.addAll(otherData)
}
searchBooks = precisionData
searchBooks = equalData
upAdapter()
}
}

@ -26,7 +26,7 @@ import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog
@ -120,7 +120,7 @@ class BookSourceEditActivity :
if (it.loginUrl.isNullOrEmpty()) {
toastOnUi(R.string.source_no_login)
} else {
startActivity<SourceLogin> {
startActivity<SourceLoginActivity> {
putExtra("sourceUrl", it.bookSourceUrl)
putExtra("loginUrl", it.loginUrl)
putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME])

@ -5,8 +5,8 @@ import android.content.Intent
import android.text.TextUtils
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import io.legado.app.BuildConfig
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
@ -164,14 +164,12 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
fun shareSelection(sources: List<BookSource>, success: ((intent: Intent) -> Unit)) {
execute {
val tmpSharePath = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(tmpSharePath)
val intent = Intent(Intent.ACTION_SEND)
val file = FileUtils.createFileWithReplace("${context.filesDir}/shareBookSource.json")
val file = FileUtils.createFileWithReplace(tmpSharePath)
file.writeText(GSON.toJson(sources))
val fileUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileProvider",
file
)
val fileUri = FileProvider.getUriForFile(context, AppConst.authority, file)
intent.type = "text/*"
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

@ -22,10 +22,10 @@ import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.viewbindingdelegate.viewBinding
class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_bookmark),
class BookmarkFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_bookmark),
BookmarkAdapter.Callback,
ChapterListViewModel.BookmarkCallBack {
override val viewModel: ChapterListViewModel by activityViewModels()
TocViewModel.BookmarkCallBack {
override val viewModel: TocViewModel by activityViewModels()
private val binding by viewBinding(FragmentBookmarkBinding::bind)
private lateinit var adapter: BookmarkAdapter
private var bookmarkLiveData: LiveData<List<Bookmark>>? = null
@ -48,29 +48,31 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
private fun initData(book: Book) {
bookmarkLiveData?.removeObservers(viewLifecycleOwner)
bookmarkLiveData = appDb.bookmarkDao.observeByBook(book.bookUrl, book.name, book.author)
bookmarkLiveData = appDb.bookmarkDao.observeByBook(book.name, book.author)
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.setItems(it) })
}
override fun startBookmarkSearch(newText: String?) {
if (newText.isNullOrBlank()) {
viewModel.bookData.value?.let {
initData(it)
viewModel.bookData.value?.let { book ->
if (newText.isNullOrBlank()) {
initData(book)
} else {
bookmarkLiveData?.removeObservers(viewLifecycleOwner)
bookmarkLiveData = appDb.bookmarkDao.liveDataSearch(book.name, book.author, newText)
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.setItems(it) })
}
} else {
bookmarkLiveData?.removeObservers(viewLifecycleOwner)
bookmarkLiveData = appDb.bookmarkDao.liveDataSearch(viewModel.bookUrl, newText)
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.setItems(it) })
}
}
override fun onClick(bookmark: Bookmark) {
val bookmarkData = Intent()
bookmarkData.putExtra("index", bookmark.chapterIndex)
bookmarkData.putExtra("chapterPos", bookmark.chapterPos)
activity?.setResult(Activity.RESULT_OK, bookmarkData)
activity?.finish()
activity?.run {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("index", bookmark.chapterIndex)
putExtra("chapterPos", bookmark.chapterPos)
})
finish()
}
}
override fun onLongClick(bookmark: Bookmark) {

@ -28,10 +28,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.min
class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_chapter_list),
class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapter_list),
ChapterListAdapter.Callback,
ChapterListViewModel.ChapterListCallBack {
override val viewModel: ChapterListViewModel by activityViewModels()
TocViewModel.ChapterListCallBack {
override val viewModel: TocViewModel by activityViewModels()
private val binding by viewBinding(FragmentChapterListBinding::bind)
lateinit var adapter: ChapterListAdapter
private var durChapterIndex = 0
@ -137,8 +137,10 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
}
override fun openChapter(bookChapter: BookChapter) {
activity?.setResult(RESULT_OK, Intent().putExtra("index", bookChapter.index))
activity?.finish()
activity?.run {
setResult(RESULT_OK, Intent().putExtra("index", bookChapter.index))
finish()
}
}
}

@ -1,14 +1,17 @@
@file:Suppress("DEPRECATION")
package io.legado.app.ui.book.toc
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.fragment.app.FragmentPagerAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityChapterListBinding
@ -19,8 +22,8 @@ import io.legado.app.utils.gone
import io.legado.app.utils.visible
class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterListViewModel>() {
override val viewModel: ChapterListViewModel
class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>() {
override val viewModel: TocViewModel
by viewModels()
private lateinit var tabLayout: TabLayout
@ -35,19 +38,14 @@ class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterLi
tabLayout.isTabIndicatorFullWidth = false
tabLayout.setSelectedTabIndicatorColor(accentColor)
binding.viewPager.adapter = TabFragmentPageAdapter()
TabLayoutMediator(tabLayout, binding.viewPager) { tab, position ->
when (position) {
0 -> tab.setText(R.string.chapter_list)
else -> tab.setText(R.string.bookmark)
}
}.attach()
tabLayout.setupWithViewPager(binding.viewPager)
intent.getStringExtra("bookUrl")?.let {
viewModel.initBook(it)
}
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.search_view, menu)
menuInflater.inflate(R.menu.book_toc, menu)
val search = menu.findItem(R.id.menu_search)
searchView = search.actionView as SearchView
ATH.setTint(searchView!!, primaryTextColor)
@ -75,19 +73,16 @@ class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterLi
return super.onCompatCreateOptionsMenu(menu)
}
private inner class TabFragmentPageAdapter : FragmentStateAdapter(this) {
override fun getItemCount(): Int {
return 2
}
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> ChapterListFragment()
else -> BookmarkFragment()
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_reverse_toc -> viewModel.reverseToc {
setResult(RESULT_OK, Intent().apply {
putExtra("index", it.durChapterIndex)
putExtra("chapterPos", it.durChapterPos)
})
}
}
return super.onCompatOptionsItemSelected(item)
}
override fun onBackPressed() {
@ -98,4 +93,29 @@ class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterLi
super.onBackPressed()
}
}
@Suppress("DEPRECATION")
private inner class TabFragmentPageAdapter :
FragmentPagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return when (position) {
1 -> BookmarkFragment()
else -> ChapterListFragment()
}
}
override fun getCount(): Int {
return 2
}
override fun getPageTitle(position: Int): CharSequence {
return when (position) {
1 -> getString(R.string.bookmark)
else -> getString(R.string.chapter_list)
}
}
}
}

@ -8,7 +8,7 @@ import androidx.activity.result.contract.ActivityResultContract
class TocActivityResult : ActivityResultContract<String, Pair<Int, Int>?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChapterListActivity::class.java)
return Intent(context, TocActivity::class.java)
.putExtra("bookUrl", input)
}

@ -7,7 +7,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
class ChapterListViewModel(application: Application) : BaseViewModel(application) {
class TocViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = ""
var bookData = MutableLiveData<Book>()
var chapterCallBack: ChapterListCallBack? = null
@ -22,6 +22,22 @@ class ChapterListViewModel(application: Application) : BaseViewModel(application
}
}
fun reverseToc(success: (book: Book) -> Unit) {
execute {
bookData.value?.apply {
setReverseToc(!getReverseToc())
val toc = appDb.bookChapterDao.getChapterList(bookUrl)
val newToc = toc.reversed()
newToc.forEachIndexed { index, bookChapter ->
bookChapter.index = index
}
appDb.bookChapterDao.insert(*newToc.toTypedArray())
}
}.onSuccess {
it?.let(success)
}
}
fun startChapterListSearch(newText: String?) {
chapterCallBack?.startChapterListSearch(newText)
}

@ -133,7 +133,7 @@ class OtherConfigFragment : BasePreferenceFragment(),
PreferKey.processText -> sharedPreferences?.let {
setProcessTextEnable(it.getBoolean(key, true))
}
PreferKey.showRss -> postEvent(EventBus.SHOW_RSS, "")
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
PreferKey.defaultCover -> upPreferenceSummary(
key, getPrefString(PreferKey.defaultCover)
)
@ -157,6 +157,11 @@ class OtherConfigFragment : BasePreferenceFragment(),
getString(R.string.pre_download_s, value)
PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value)
PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value)
PreferKey.defaultCover -> preference.summary = if (value.isNullOrBlank()) {
getString(R.string.select_image)
} else {
value
}
else -> if (preference is ListPreference) {
val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value.

@ -15,7 +15,7 @@ import io.legado.app.help.http.CookieStore
import io.legado.app.utils.snackbar
class SourceLogin : BaseActivity<ActivitySourceLoginBinding>() {
class SourceLoginActivity : BaseActivity<ActivitySourceLoginBinding>() {
var sourceUrl: String? = null
var loginUrl: String? = null

@ -1,12 +1,16 @@
@file:Suppress("DEPRECATION")
package io.legado.app.ui.main
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuItem
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.BuildConfig
import io.legado.app.R
@ -23,7 +27,9 @@ import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.elevation
import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.main.bookshelf.BookshelfFragment
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
import io.legado.app.ui.main.bookshelf.style1.BookshelfFragment1
import io.legado.app.ui.main.bookshelf.style2.BookshelfFragment2
import io.legado.app.ui.main.explore.ExploreFragment
import io.legado.app.ui.main.my.MyFragment
import io.legado.app.ui.main.rss.RssFragment
@ -40,36 +46,26 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
private var bookshelfReselected: Long = 0
private var exploreReselected: Long = 0
private var pagePosition = 0
private val fragmentMap = hashMapOf<Long, Fragment>()
private val fragmentMap = hashMapOf<Int, Fragment>()
private var bottomMenuCount = 2
private val realPositions = arrayOf(0, 1, 2, 3)
override fun getViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
upBottomMenu()
binding.apply {
ATH.applyEdgeEffectColor(viewPagerMain)
ATH.applyBottomNavigationColor(bottomNavigationView)
viewPagerMain.offscreenPageLimit = 3
viewPagerMain.adapter = TabFragmentPageAdapter()
viewPagerMain.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
pagePosition = position
when (position) {
0, 1, 3 -> bottomNavigationView.menu.getItem(position).isChecked = true
2 -> if (AppConfig.isShowRSS) {
bottomNavigationView.menu.getItem(position).isChecked = true
} else {
bottomNavigationView.menu.getItem(3).isChecked = true
}
}
}
})
viewPagerMain.adapter = TabFragmentPageAdapter(supportFragmentManager)
viewPagerMain.addOnPageChangeListener(PageChangeCallback())
bottomNavigationView.elevation =
if (AppConfig.elevation < 0) elevation else AppConfig.elevation.toFloat()
bottomNavigationView.setOnNavigationItemSelectedListener(this@MainActivity)
bottomNavigationView.setOnNavigationItemReselectedListener(this@MainActivity)
bottomNavigationView.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
}
}
@ -90,9 +86,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
override fun onNavigationItemSelected(item: MenuItem): Boolean = with(binding) {
when (item.itemId) {
R.id.menu_bookshelf -> viewPagerMain.setCurrentItem(0, false)
R.id.menu_explore -> viewPagerMain.setCurrentItem(1, false)
R.id.menu_rss -> viewPagerMain.setCurrentItem(2, false)
R.id.menu_my_config -> viewPagerMain.setCurrentItem(3, false)
R.id.menu_discovery -> viewPagerMain.setCurrentItem(1, false)
R.id.menu_rss -> if (AppConfig.showDiscovery) {
viewPagerMain.setCurrentItem(2, false)
} else {
viewPagerMain.setCurrentItem(1, false)
}
R.id.menu_my_config -> viewPagerMain.setCurrentItem(bottomMenuCount - 1, false)
}
return false
}
@ -103,10 +103,10 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
if (System.currentTimeMillis() - bookshelfReselected > 300) {
bookshelfReselected = System.currentTimeMillis()
} else {
(fragmentMap[0] as? BookshelfFragment)?.gotoTop()
(fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.gotoTop()
}
}
R.id.menu_explore -> {
R.id.menu_discovery -> {
if (System.currentTimeMillis() - exploreReselected > 300) {
exploreReselected = System.currentTimeMillis()
} else {
@ -116,10 +116,6 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
}
}
fun getViewPager(): ViewPager2 {
return binding.viewPagerMain
}
private fun upVersion() {
if (LocalConfig.versionCode != appInfo.versionCode) {
LocalConfig.versionCode = appInfo.versionCode
@ -176,12 +172,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
observeEvent<String>(EventBus.RECREATE) {
recreate()
}
observeEvent<String>(EventBus.SHOW_RSS) {
binding.bottomNavigationView.menu.findItem(R.id.menu_rss).isVisible =
AppConfig.isShowRSS
binding.viewPagerMain.adapter?.notifyDataSetChanged()
if (AppConfig.isShowRSS) {
binding.viewPagerMain.setCurrentItem(3, false)
observeEvent<Boolean>(EventBus.NOTIFY_MAIN) {
binding.apply {
upBottomMenu()
viewPagerMain.adapter?.notifyDataSetChanged()
if (it) {
viewPagerMain.setCurrentItem(bottomMenuCount - 1, false)
}
}
}
observeEvent<String>(PreferKey.threadCount) {
@ -189,33 +186,81 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
}
}
private inner class TabFragmentPageAdapter :
FragmentStateAdapter(this) {
override fun getItemId(position: Int): Long {
return when (position) {
2 -> if (AppConfig.isShowRSS) 2 else 3
else -> position.toLong()
private fun upBottomMenu() {
val showDiscovery = AppConfig.showDiscovery
val showRss = AppConfig.showRSS
binding.bottomNavigationView.menu.let { menu ->
menu.findItem(R.id.menu_discovery).isVisible = showDiscovery
menu.findItem(R.id.menu_rss).isVisible = showRss
}
bottomMenuCount = 2
realPositions[1] = 1
realPositions[2] = 2
when {
showDiscovery -> bottomMenuCount++
showRss -> {
realPositions[1] = 2
realPositions[2] = 3
}
else -> {
realPositions[1] = 3
realPositions[2] = 3
}
}
if (showRss) {
bottomMenuCount++
} else {
realPositions[2] = 3
}
}
override fun containsItem(itemId: Long): Boolean {
return fragmentMap.containsKey(itemId)
private fun getFragmentId(position: Int): Int {
val p = realPositions[position]
if (p == 0) {
return if (AppConfig.bookGroupStyle == 1) 11 else 0
}
return p
}
private inner class PageChangeCallback : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
pagePosition = position
binding.bottomNavigationView.menu
.getItem(realPositions[position]).isChecked = true
}
}
@Suppress("DEPRECATION")
private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemCount(): Int {
return if (AppConfig.isShowRSS) 4 else 3
private fun getId(position: Int): Int {
return getFragmentId(position)
}
override fun createFragment(position: Int): Fragment {
val id = getItemId(position)
val fragment = when (id) {
0L -> BookshelfFragment()
1L -> ExploreFragment()
2L -> RssFragment()
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getItem(position: Int): Fragment {
return when (getId(position)) {
0 -> BookshelfFragment1()
11 -> BookshelfFragment2()
1 -> ExploreFragment()
2 -> RssFragment()
else -> MyFragment()
}
fragmentMap[id] = fragment
}
override fun getCount(): Int {
return bottomMenuCount
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as Fragment
fragmentMap[getId(position)] = fragment
return fragment
}

@ -0,0 +1,140 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.databinding.DialogBookshelfConfigBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.book.arrange.ArrangeBookActivity
import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.main.MainViewModel
import io.legado.app.utils.*
abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment<BookshelfViewModel>(layoutId) {
val activityViewModel: MainViewModel by activityViewModels()
override val viewModel: BookshelfViewModel by viewModels()
private val importBookshelf = registerForActivityResult(FilePicker()) {
it?.readText(requireContext())?.let { text ->
viewModel.importBookshelf(text, groupId)
}
}
abstract val groupId: Long
abstract val books: List<Book>
abstract fun gotoTop()
override fun onCompatOptionsItemSelected(item: MenuItem) {
super.onCompatOptionsItemSelected(item)
when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_update_toc -> activityViewModel.upToc(books)
R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> addBookByUrl()
R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity> {
putExtra("groupId", groupId)
}
R.id.menu_download -> startActivity<CacheActivity> {
putExtra("groupId", groupId)
}
R.id.menu_export_bookshelf -> viewModel.exportBookshelf(books) {
activity?.share(it)
}
R.id.menu_import_bookshelf -> importBookshelfAlert(groupId)
}
}
@SuppressLint("InflateParams")
fun addBookByUrl() {
alert(titleResource = R.string.add_book_url) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let {
viewModel.addBookByUrl(it)
}
}
noButton()
}.show()
}
@SuppressLint("InflateParams")
fun configBookshelf() {
alert(titleResource = R.string.bookshelf_layout) {
val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
val bookshelfSort = getPrefInt(PreferKey.bookshelfSort)
val alertBinding =
DialogBookshelfConfigBinding.inflate(layoutInflater)
.apply {
spGroupStyle.setSelection(AppConfig.bookGroupStyle)
rgLayout.checkByIndex(bookshelfLayout)
rgSort.checkByIndex(bookshelfSort)
}
customView { alertBinding.root }
okButton {
alertBinding.apply {
var changed = false
if (AppConfig.bookGroupStyle != spGroupStyle.selectedItemPosition) {
AppConfig.bookGroupStyle = spGroupStyle.selectedItemPosition
postEvent(EventBus.NOTIFY_MAIN, false)
}
if (bookshelfLayout != rgLayout.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfLayout, rgLayout.getCheckedIndex())
changed = true
}
if (bookshelfSort != rgSort.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfSort, rgSort.getCheckedIndex())
changed = true
}
if (changed) {
postEvent(EventBus.RECREATE, "")
}
}
}
noButton()
}.show()
}
private fun importBookshelfAlert(groupId: Long) {
alert(titleResource = R.string.import_bookshelf) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "url/json"
}
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let {
viewModel.importBookshelf(it, groupId)
}
}
noButton()
neutralButton(R.string.select_file) {
importBookshelf.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
}
}.show()
}
}

@ -1,278 +0,0 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogBookshelfConfigBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.FragmentBookshelfBinding
import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.arrange.ArrangeBookActivity
import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
/**
* 书架界面
*/
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener,
SearchView.OnQueryTextListener {
private val binding by viewBinding(FragmentBookshelfBinding::bind)
override val viewModel: BookshelfViewModel by viewModels()
private val activityViewModel: MainViewModel by activityViewModels()
private lateinit var adapter: FragmentStateAdapter
private lateinit var tabLayout: TabLayout
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Long, BooksFragment>()
private val importBookshelf = registerForActivityResult(FilePicker()) {
it?.readText(requireContext())?.let { text ->
viewModel.importBookshelf(text, selectedGroup.groupId)
}
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tabLayout = binding.titleBar.findViewById(R.id.tab_layout)
setSupportToolbar(binding.titleBar.toolbar)
initView()
initBookGroupData()
}
override fun onCompatCreateOptionsMenu(menu: Menu) {
menuInflater.inflate(R.menu.main_bookshelf, menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem) {
super.onCompatOptionsItemSelected(item)
when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_update_toc -> {
val fragment = fragmentMap[selectedGroup.groupId]
fragment?.getBooks()?.let {
activityViewModel.upToc(it)
}
}
R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> addBookByUrl()
R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity> {
putExtra("groupId", selectedGroup.groupId)
putExtra("groupName", selectedGroup.groupName)
}
R.id.menu_download -> startActivity<CacheActivity> {
putExtra("groupId", selectedGroup.groupId)
putExtra("groupName", selectedGroup.groupName)
}
R.id.menu_export_bookshelf -> {
val fragment = fragmentMap[selectedGroup.groupId]
viewModel.exportBookshelf(fragment?.getBooks()) {
activity?.share(it)
}
}
R.id.menu_import_bookshelf -> importBookshelfAlert()
}
}
private val selectedGroup: BookGroup
get() = bookGroups[tabLayout.selectedTabPosition]
private fun initView() {
ATH.applyEdgeEffectColor(binding.viewPagerBookshelf)
tabLayout.isTabIndicatorFullWidth = false
tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
tabLayout.setSelectedTabIndicatorColor(requireContext().accentColor)
binding.viewPagerBookshelf.offscreenPageLimit = 1
adapter = TabFragmentPageAdapter()
binding.viewPagerBookshelf.adapter = adapter
TabLayoutMediator(tabLayout, binding.viewPagerBookshelf) { tab, i ->
tab.text = bookGroups[i].groupName
}.attach()
}
private fun initBookGroupData() {
bookGroupLiveData?.removeObservers(viewLifecycleOwner)
bookGroupLiveData = appDb.bookGroupDao.liveDataShow().apply {
observe(viewLifecycleOwner) {
viewModel.checkGroup(it)
upGroup(it)
}
}
}
override fun onQueryTextSubmit(query: String?): Boolean {
startActivity<SearchActivity> {
putExtra("key", query)
}
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
@Synchronized
private fun upGroup(data: List<BookGroup>) {
if (data.isEmpty()) {
appDb.bookGroupDao.enableGroup(AppConst.bookGroupAllId)
} else {
if (data != bookGroups) {
bookGroups.clear()
bookGroups.addAll(data)
adapter.notifyDataSetChanged()
selectLastTab()
}
}
}
@Synchronized
private fun selectLastTab() {
tabLayout.removeOnTabSelectedListener(this)
tabLayout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select()
tabLayout.addOnTabSelectedListener(this)
}
@SuppressLint("InflateParams")
private fun configBookshelf() {
alert(titleResource = R.string.bookshelf_layout) {
val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
val bookshelfSort = getPrefInt(PreferKey.bookshelfSort)
val alertBinding =
DialogBookshelfConfigBinding.inflate(layoutInflater)
.apply {
rgLayout.checkByIndex(bookshelfLayout)
rgSort.checkByIndex(bookshelfSort)
swShowUnread.isChecked = AppConfig.showUnread
}
customView { alertBinding.root }
okButton {
alertBinding.apply {
var changed = false
if (bookshelfLayout != rgLayout.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfLayout, rgLayout.getCheckedIndex())
changed = true
}
if (bookshelfSort != rgSort.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfSort, rgSort.getCheckedIndex())
changed = true
}
if (AppConfig.showUnread != swShowUnread.isChecked) {
AppConfig.showUnread = swShowUnread.isChecked
changed = true
}
if (changed) {
activity?.recreate()
}
}
}
noButton()
}.show()
}
@SuppressLint("InflateParams")
private fun addBookByUrl() {
alert(titleResource = R.string.add_book_url) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let {
viewModel.addBookByUrl(it)
}
}
noButton()
}.show()
}
override fun onTabReselected(tab: TabLayout.Tab) {
fragmentMap[selectedGroup.groupId]?.let {
toastOnUi("${selectedGroup.groupName}(${it.getBooksCount()})")
}
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabSelected(tab: TabLayout.Tab) {
putPrefInt(PreferKey.saveTabPosition, tab.position)
}
fun gotoTop() {
fragmentMap[selectedGroup.groupId]?.gotoTop()
}
private fun importBookshelfAlert() {
alert(titleResource = R.string.import_bookshelf) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "url/json"
}
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let {
viewModel.importBookshelf(it, selectedGroup.groupId)
}
}
noButton()
neutralButton(R.string.select_file) {
importBookshelf.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
}
}.show()
}
private inner class TabFragmentPageAdapter :
FragmentStateAdapter(this) {
override fun getItemId(position: Int): Long {
val group = bookGroups[position]
return group.groupId
}
override fun containsItem(itemId: Long): Boolean {
return fragmentMap.containsKey(itemId)
}
override fun getItemCount(): Int {
return bookGroups.size
}
override fun createFragment(position: Int): Fragment {
val group = bookGroups[position]
val fragment = BooksFragment.newInstance(position, group.groupId)
fragmentMap[group.groupId] = fragment
return fragment
}
}
}

@ -1,82 +0,0 @@
package io.legado.app.ui.main.bookshelf
import android.content.Context
import android.gesture.GestureOverlayView.ORIENTATION_HORIZONTAL
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.LinearLayout
import androidx.viewpager2.widget.ViewPager2
import io.legado.app.R
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.activity
import kotlin.math.absoluteValue
import kotlin.math.sign
class RootView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() = (activity as? MainActivity)?.getViewPager()
private val childViewPager: View?
get() = findViewById(R.id.view_pager_bookshelf)
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> childViewPager?.canScrollHorizontally(direction) ?: false
1 -> childViewPager?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

@ -0,0 +1,166 @@
@file:Suppress("DEPRECATION")
package io.legado.app.ui.main.bookshelf.style1
import android.os.Bundle
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.lifecycle.LiveData
import com.google.android.material.tabs.TabLayout
import io.legado.app.R
import io.legado.app.constant.AppConst
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.databinding.FragmentBookshelfBinding
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
import io.legado.app.ui.main.bookshelf.style1.books.BooksFragment
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
/**
* 书架界面
*/
class BookshelfFragment1 : BaseBookshelfFragment(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener,
SearchView.OnQueryTextListener {
private val binding by viewBinding(FragmentBookshelfBinding::bind)
private lateinit var adapter: FragmentStatePagerAdapter
private lateinit var tabLayout: TabLayout
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Long, BooksFragment>()
override val groupId: Long get() = selectedGroup.groupId
override val books: List<Book>
get() {
val fragment = fragmentMap[selectedGroup.groupId]
return fragment?.getBooks() ?: emptyList()
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tabLayout = binding.titleBar.findViewById(R.id.tab_layout)
setSupportToolbar(binding.titleBar.toolbar)
initView()
initBookGroupData()
}
override fun onCompatCreateOptionsMenu(menu: Menu) {
menuInflater.inflate(R.menu.main_bookshelf, menu)
}
private val selectedGroup: BookGroup
get() = bookGroups[tabLayout.selectedTabPosition]
private fun initView() {
ATH.applyEdgeEffectColor(binding.viewPagerBookshelf)
tabLayout.isTabIndicatorFullWidth = false
tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
tabLayout.setSelectedTabIndicatorColor(requireContext().accentColor)
tabLayout.setupWithViewPager(binding.viewPagerBookshelf)
binding.viewPagerBookshelf.offscreenPageLimit = 1
adapter = TabFragmentPageAdapter(childFragmentManager)
binding.viewPagerBookshelf.adapter = adapter
}
private fun initBookGroupData() {
bookGroupLiveData?.removeObservers(viewLifecycleOwner)
bookGroupLiveData = appDb.bookGroupDao.liveDataShow().apply {
observe(viewLifecycleOwner) {
viewModel.checkGroup(it)
upGroup(it)
}
}
}
override fun onQueryTextSubmit(query: String?): Boolean {
startActivity<SearchActivity> {
putExtra("key", query)
}
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
@Synchronized
private fun upGroup(data: List<BookGroup>) {
if (data.isEmpty()) {
appDb.bookGroupDao.enableGroup(AppConst.bookGroupAllId)
} else {
if (data != bookGroups) {
bookGroups.clear()
bookGroups.addAll(data)
adapter.notifyDataSetChanged()
selectLastTab()
}
}
}
@Synchronized
private fun selectLastTab() {
tabLayout.removeOnTabSelectedListener(this)
tabLayout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select()
tabLayout.addOnTabSelectedListener(this)
}
override fun onTabReselected(tab: TabLayout.Tab) {
fragmentMap[selectedGroup.groupId]?.let {
toastOnUi("${selectedGroup.groupName}(${it.getBooksCount()})")
}
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabSelected(tab: TabLayout.Tab) {
putPrefInt(PreferKey.saveTabPosition, tab.position)
}
override fun gotoTop() {
fragmentMap[selectedGroup.groupId]?.gotoTop()
}
private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getPageTitle(position: Int): CharSequence {
return bookGroups[position].groupName
}
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getItem(position: Int): Fragment {
val group = bookGroups[position]
return BooksFragment.newInstance(position, group.groupId)
}
override fun getCount(): Int {
return bookGroups.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as BooksFragment
val group = bookGroups[position]
fragmentMap[group.groupId] = fragment
return fragment
}
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.main.bookshelf.books
package io.legado.app.ui.main.bookshelf.style1.books
import android.content.Context
import androidx.core.os.bundleOf

@ -1,4 +1,4 @@
package io.legado.app.ui.main.bookshelf.books
package io.legado.app.ui.main.bookshelf.style1.books
import android.content.Context
import android.os.Bundle

@ -1,4 +1,4 @@
package io.legado.app.ui.main.bookshelf.books
package io.legado.app.ui.main.bookshelf.style1.books
import android.content.Context
import android.os.Bundle

@ -1,4 +1,4 @@
package io.legado.app.ui.main.bookshelf.books
package io.legado.app.ui.main.bookshelf.style1.books
import android.os.Bundle
import android.view.View
@ -18,7 +18,6 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.databinding.FragmentBooksBinding
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.audio.AudioPlayActivity
@ -149,7 +148,6 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
}
else -> startActivity<ReadBookActivity> {
putExtra("bookUrl", book.bookUrl)
putExtra("key", IntentDataHelp.putData(book))
}
}
}

@ -0,0 +1,85 @@
package io.legado.app.ui.main.bookshelf.style2
import android.content.Context
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.data.entities.Book
abstract class BaseBooksAdapter<VH : RecyclerView.ViewHolder>(
val context: Context,
val callBack: CallBack
) : RecyclerView.Adapter<VH>() {
val diffItemCallback: DiffUtil.ItemCallback<Book>
get() = object : DiffUtil.ItemCallback<Book>() {
override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
return oldItem.name == newItem.name
&& oldItem.author == newItem.author
}
override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
return when {
oldItem.durChapterTime != newItem.durChapterTime -> false
oldItem.name != newItem.name -> false
oldItem.author != newItem.author -> false
oldItem.durChapterTitle != newItem.durChapterTitle -> false
oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
oldItem.lastCheckCount != newItem.lastCheckCount -> false
oldItem.getDisplayCover() != newItem.getDisplayCover() -> false
oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false
else -> true
}
}
override fun getChangePayload(oldItem: Book, newItem: Book): Any? {
val bundle = bundleOf()
if (oldItem.name != newItem.name) {
bundle.putString("name", newItem.name)
}
if (oldItem.author != newItem.author) {
bundle.putString("author", newItem.author)
}
if (oldItem.durChapterTitle != newItem.durChapterTitle) {
bundle.putString("dur", newItem.durChapterTitle)
}
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
bundle.putString("last", newItem.latestChapterTitle)
}
if (oldItem.getDisplayCover() != newItem.getDisplayCover()) {
bundle.putString("cover", newItem.getDisplayCover())
}
if (oldItem.lastCheckCount != newItem.lastCheckCount
|| oldItem.durChapterTime != newItem.durChapterTime
|| oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()
|| oldItem.lastCheckCount != newItem.lastCheckCount
) {
bundle.putBoolean("refresh", true)
}
if (bundle.isEmpty) return null
return bundle
}
}
fun notification(bookUrl: String) {
for (i in 0 until itemCount) {
callBack.getItem(i).let {
if (it is Book && it.bookUrl == bookUrl) {
notifyItemChanged(i, bundleOf(Pair("refresh", null)))
return
}
}
}
}
interface CallBack {
fun onItemClick(position: Int)
fun onItemLongClick(position: Int)
fun isUpdate(bookUrl: String): Boolean
fun getItemCount(): Int
fun getItem(position: Int): Any
}
}

@ -0,0 +1,134 @@
package io.legado.app.ui.main.bookshelf.style2
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.ItemBookshelfGridBinding
import io.legado.app.databinding.ItemBookshelfGridGroupBinding
import io.legado.app.help.AppConfig
import io.legado.app.utils.invisible
import splitties.views.onLongClick
class BooksAdapterGrid(context: Context, callBack: CallBack) :
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
override fun getItemCount(): Int {
return callBack.getItemCount()
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return BookViewHolder(
ItemBookshelfGridBinding.inflate(LayoutInflater.from(context), parent, false)
)
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
val bundle = payloads.getOrNull(0) as? Bundle
when {
bundle == null -> super.onBindViewHolder(holder, position, payloads)
holder is BookViewHolder -> onBindBook(holder.binding, position, bundle)
holder is GroupViewHolder -> onBindGroup(holder.binding, position, bundle)
}
}
private fun onBindGroup(binding: ItemBookshelfGridGroupBinding, position: Int, bundle: Bundle) {
binding.run {
val item = callBack.getItem(position)
if (item is BookGroup) {
tvName.text = item.groupName
}
}
}
private fun onBindBook(binding: ItemBookshelfGridBinding, position: Int, bundle: Bundle) {
binding.run {
val item = callBack.getItem(position)
if (item is Book) {
bundle.keySet().forEach {
when (it) {
"name" -> tvName.text = item.name
"cover" -> ivCover.load(
item.getDisplayCover(),
item.name,
item.author
)
"refresh" -> upRefresh(this, item)
}
}
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is BookViewHolder -> onBindBook(holder.binding, position)
is GroupViewHolder -> onBindGroup(holder.binding, position)
}
}
private fun onBindGroup(binding: ItemBookshelfGridGroupBinding, position: Int) {
binding.run {
val item = callBack.getItem(position)
if (item is BookGroup) {
tvName.text = item.groupName
}
root.setOnClickListener {
callBack.onItemClick(position)
}
root.onLongClick {
callBack.onItemLongClick(position)
}
}
}
private fun onBindBook(binding: ItemBookshelfGridBinding, position: Int) {
binding.run {
val item = callBack.getItem(position)
if (item is Book) {
tvName.text = item.name
ivCover.load(item.getDisplayCover(), item.name, item.author)
upRefresh(this, item)
}
root.setOnClickListener {
callBack.onItemClick(position)
}
root.onLongClick {
callBack.onItemLongClick(position)
}
}
}
private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) {
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
binding.bvUnread.invisible()
binding.rlLoading.show()
} else {
binding.rlLoading.hide()
if (AppConfig.showUnread) {
binding.bvUnread.setBadgeCount(item.getUnreadChapterNum())
binding.bvUnread.setHighlight(item.lastCheckCount > 0)
} else {
binding.bvUnread.invisible()
}
}
}
class BookViewHolder(val binding: ItemBookshelfGridBinding) :
RecyclerView.ViewHolder(binding.root)
class GroupViewHolder(val binding: ItemBookshelfGridGroupBinding) :
RecyclerView.ViewHolder(binding.root)
}

@ -0,0 +1,150 @@
package io.legado.app.ui.main.bookshelf.style2
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.ItemBookshelfListBinding
import io.legado.app.databinding.ItemBookshelfListGroupBinding
import io.legado.app.help.AppConfig
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import splitties.views.onLongClick
class BooksAdapterList(context: Context, callBack: CallBack) :
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
override fun getItemCount(): Int {
return callBack.getItemCount()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return BookViewHolder(
ItemBookshelfListBinding.inflate(LayoutInflater.from(context), parent, false)
)
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
val bundle = payloads.getOrNull(0) as? Bundle
when {
bundle == null -> super.onBindViewHolder(holder, position, payloads)
holder is BookViewHolder -> onBindBook(holder.binding, position, bundle)
holder is GroupViewHolder -> onBindGroup(holder.binding, position, bundle)
}
}
private fun onBindGroup(binding: ItemBookshelfListGroupBinding, position: Int, bundle: Bundle) {
binding.run {
val item = callBack.getItem(position)
if (item is BookGroup) {
tvName.text = item.groupName
}
}
}
private fun onBindBook(binding: ItemBookshelfListBinding, position: Int, bundle: Bundle) {
binding.run {
val item = callBack.getItem(position)
if (item is Book) {
tvRead.text = item.durChapterTitle
tvLast.text = item.latestChapterTitle
bundle.keySet().forEach {
when (it) {
"name" -> tvName.text = item.name
"author" -> tvAuthor.text = item.author
"cover" -> ivCover.load(
item.getDisplayCover(),
item.name,
item.author
)
"refresh" -> upRefresh(this, item)
}
}
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is BookViewHolder -> onBindBook(holder.binding, position)
is GroupViewHolder -> onBindGroup(holder.binding, position)
}
}
private fun onBindGroup(binding: ItemBookshelfListGroupBinding, position: Int) {
binding.run {
val item = callBack.getItem(position)
if (item is BookGroup) {
tvName.text = item.groupName
flHasNew.gone()
ivAuthor.gone()
ivLast.gone()
ivRead.gone()
tvAuthor.gone()
tvLast.gone()
tvRead.gone()
}
root.setOnClickListener {
callBack.onItemClick(position)
}
root.onLongClick {
callBack.onItemLongClick(position)
}
}
}
private fun onBindBook(binding: ItemBookshelfListBinding, position: Int) {
binding.run {
val item = callBack.getItem(position)
if (item is Book) {
tvName.text = item.name
tvAuthor.text = item.author
tvRead.text = item.durChapterTitle
tvLast.text = item.latestChapterTitle
ivCover.load(item.getDisplayCover(), item.name, item.author)
flHasNew.visible()
ivAuthor.visible()
ivLast.visible()
ivRead.visible()
upRefresh(this, item)
}
root.setOnClickListener {
callBack.onItemClick(position)
}
root.onLongClick {
callBack.onItemLongClick(position)
}
}
}
private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) {
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
binding.bvUnread.invisible()
binding.rlLoading.show()
} else {
binding.rlLoading.hide()
if (AppConfig.showUnread) {
binding.bvUnread.setHighlight(item.lastCheckCount > 0)
binding.bvUnread.setBadgeCount(item.getUnreadChapterNum())
} else {
binding.bvUnread.invisible()
}
}
}
class BookViewHolder(val binding: ItemBookshelfListBinding) :
RecyclerView.ViewHolder(binding.root)
class GroupViewHolder(val binding: ItemBookshelfListGroupBinding) :
RecyclerView.ViewHolder(binding.root)
}

@ -0,0 +1,235 @@
package io.legado.app.ui.main.bookshelf.style2
import android.os.Bundle
import android.view.Menu
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType
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.BookGroup
import io.legado.app.databinding.FragmentBookshelf1Binding
import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
import io.legado.app.utils.cnCompare
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.observeEvent
import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlin.math.max
/**
* 书架界面
*/
class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
SearchView.OnQueryTextListener,
BaseBooksAdapter.CallBack {
private val binding by viewBinding(FragmentBookshelf1Binding::bind)
private lateinit var searchView: SearchView
private lateinit var booksAdapter: BaseBooksAdapter<*>
override var groupId = AppConst.bookGroupNoneId
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private var bookshelfLiveData: LiveData<List<Book>>? = null
private var bookGroups: List<BookGroup> = emptyList()
override var books: List<Book> = emptyList()
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
searchView = binding.titleBar.findViewById(R.id.search_view)
setSupportToolbar(binding.titleBar.toolbar)
initSearchView()
initRecyclerView()
initGroupData()
initBooksData()
}
override fun onCompatCreateOptionsMenu(menu: Menu) {
menuInflater.inflate(R.menu.main_bookshelf, menu)
menu.findItem(R.id.menu_search).isVisible = false
}
private fun initSearchView() {
ATH.setTint(searchView, primaryTextColor)
searchView.onActionViewExpanded()
searchView.isSubmitButtonEnabled = true
searchView.queryHint = getString(R.string.screen_find)
searchView.clearFocus()
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
})
}
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(binding.rvBookshelf)
binding.refreshLayout.setColorSchemeColors(accentColor)
binding.refreshLayout.setOnRefreshListener {
binding.refreshLayout.isRefreshing = false
activityViewModel.upToc(books)
}
val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
if (bookshelfLayout == 0) {
binding.rvBookshelf.layoutManager = LinearLayoutManager(context)
booksAdapter = BooksAdapterList(requireContext(), this)
} else {
binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2)
booksAdapter = BooksAdapterGrid(requireContext(), this)
}
binding.rvBookshelf.adapter = booksAdapter
booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
val layoutManager = binding.rvBookshelf.layoutManager
if (positionStart == 0 && layoutManager is LinearLayoutManager) {
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
val layoutManager = binding.rvBookshelf.layoutManager
if (toPosition == 0 && layoutManager is LinearLayoutManager) {
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
}
}
})
}
private fun initGroupData() {
bookGroupLiveData?.removeObservers(this)
bookGroupLiveData = appDb.bookGroupDao.liveDataShow().apply {
observe(viewLifecycleOwner) {
if (it != bookGroups) {
bookGroups = it
booksAdapter.notifyDataSetChanged()
}
}
}
}
private fun initBooksData() {
bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = when (groupId) {
AppConst.bookGroupAllId -> appDb.bookDao.observeAll()
AppConst.bookGroupLocalId -> appDb.bookDao.observeLocal()
AppConst.bookGroupAudioId -> appDb.bookDao.observeAudio()
AppConst.bookGroupNoneId -> appDb.bookDao.observeNoGroup()
else -> appDb.bookDao.observeByGroup(groupId)
}.apply {
observe(viewLifecycleOwner) { list ->
binding.tvEmptyMsg.isGone = list.isNotEmpty()
books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending {
it.latestChapterTime
}
2 -> list.sortedWith { o1, o2 ->
o1.name.cnCompare(o2.name)
}
3 -> list.sortedBy {
it.order
}
else -> list.sortedByDescending {
it.durChapterTime
}
}
booksAdapter.notifyDataSetChanged()
}
}
}
override fun onQueryTextSubmit(query: String?): Boolean {
startActivity<SearchActivity> {
putExtra("key", query)
}
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
override fun gotoTop() {
if (AppConfig.isEInkMode) {
binding.rvBookshelf.scrollToPosition(0)
} else {
binding.rvBookshelf.smoothScrollToPosition(0)
}
}
override fun onItemClick(position: Int) {
if (position < bookGroups.size) {
val bookGroup = bookGroups[position]
groupId = bookGroup.groupId
initBooksData()
} else {
val book = books[position - bookGroups.size]
when (book.type) {
BookType.audio ->
startActivity<AudioPlayActivity> {
putExtra("bookUrl", book.bookUrl)
}
else -> startActivity<ReadBookActivity> {
putExtra("bookUrl", book.bookUrl)
}
}
}
}
override fun onItemLongClick(position: Int) {
if (position < bookGroups.size) {
val bookGroup = bookGroups[position]
} else {
val book = books[position - bookGroups.size]
startActivity<BookInfoActivity> {
putExtra("name", book.name)
putExtra("author", book.author)
}
}
}
override fun isUpdate(bookUrl: String): Boolean {
return bookUrl in activityViewModel.updateList
}
override fun getItemCount(): Int {
return bookGroups.size + books.size
}
override fun getItem(position: Int): Any {
return if (position < bookGroups.size) {
bookGroups[position]
} else {
books[position - bookGroups.size]
}
}
override fun observeLiveBus() {
super.observeLiveBus()
observeEvent<String>(EventBus.UP_BOOK) {
booksAdapter.notification(it)
}
}
}

@ -2,9 +2,28 @@ package io.legado.app.ui.qrcode
import com.google.zxing.Result
import com.king.zxing.CaptureFragment
import com.king.zxing.DecodeConfig
import com.king.zxing.DecodeFormatManager
import com.king.zxing.analyze.MultiFormatAnalyzer
class QrCodeFragment : CaptureFragment() {
override fun initCameraScan() {
super.initCameraScan()
//初始化解码配置
val decodeConfig = DecodeConfig()
//如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS
decodeConfig.hints = DecodeFormatManager.QR_CODE_HINTS
//设置是否全区域识别,默认false
decodeConfig.isFullAreaScan = true
//设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别
decodeConfig.areaRectRatio = 0.8f
//在启动预览之前,设置分析器,只识别二维码
cameraScan.setAnalyzer(MultiFormatAnalyzer(decodeConfig))
}
override fun onScanResultCallback(result: Result?): Boolean {
(activity as? QrCodeActivity)?.onScanResultCallback(result)
return true

@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package io.legado.app.ui.rss.article
import android.content.Intent
@ -7,8 +9,7 @@ import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import androidx.fragment.app.FragmentStatePagerAdapter
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityRssArtivlesBinding
@ -20,9 +21,8 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
override val viewModel: RssSortViewModel
by viewModels()
private var sorts = linkedMapOf<String, String>()
private lateinit var adapter: TabFragmentPageAdapter
private val fragmentMap = hashMapOf<Long, Fragment>()
private val fragments = linkedMapOf<String, Fragment>()
private val upSourceResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
@ -40,9 +40,7 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
override fun onActivityCreated(savedInstanceState: Bundle?) {
adapter = TabFragmentPageAdapter()
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = sorts.keys.elementAt(position)
}.attach()
binding.tabLayout.setupWithViewPager(binding.viewPager)
viewModel.titleLiveData.observe(this, {
binding.titleBar.title = it
})
@ -78,10 +76,11 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
}
private fun upFragments() {
viewModel.rssSource?.sortUrls()?.let {
sorts = it
fragments.clear()
viewModel.rssSource?.sortUrls()?.forEach {
fragments[it.key] = RssArticlesFragment.create(it.key, it.value)
}
if (sorts.size == 1) {
if (fragments.size == 1) {
binding.tabLayout.gone()
} else {
binding.tabLayout.visible()
@ -89,31 +88,25 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
adapter.notifyDataSetChanged()
}
private inner class TabFragmentPageAdapter : FragmentStateAdapter(this) {
private inner class TabFragmentPageAdapter :
FragmentStatePagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemCount(): Int {
return sorts.size
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getItemId(position: Int): Long {
val style = viewModel.rssSource?.articleStyle ?: 0
return style * 100 + super.getItemId(position)
override fun getPageTitle(position: Int): CharSequence {
return fragments.keys.elementAt(position)
}
override fun containsItem(itemId: Long): Boolean {
return fragmentMap.containsKey(itemId)
override fun getItem(position: Int): Fragment {
return fragments.values.elementAt(position)
}
override fun createFragment(position: Int): Fragment {
val itemId = getItemId(position)
val fragment = RssArticlesFragment.create(
sorts.keys.elementAt(position),
sorts.values.elementAt(position)
)
fragmentMap[itemId] = fragment
return fragment
override fun getCount(): Int {
return fragments.size
}
}
}

@ -21,6 +21,7 @@ import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemGroupManageBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.*
@ -128,6 +129,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
payloads: MutableList<Any>
) {
with(binding) {
root.setBackgroundColor(context.backgroundColor)
tvGroup.text = item
}
}

@ -5,8 +5,8 @@ import android.content.Intent
import android.text.TextUtils
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import io.legado.app.BuildConfig
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource
import io.legado.app.help.DefaultData
@ -106,14 +106,12 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
fun shareSelection(sources: List<RssSource>, success: ((intent: Intent) -> Unit)) {
execute {
val tmpSharePath = "${context.filesDir}/shareRssSource.json"
FileUtils.delete(tmpSharePath)
val intent = Intent(Intent.ACTION_SEND)
val file = FileUtils.createFileWithReplace("${context.filesDir}/shareRssSource.json")
val file = FileUtils.createFileWithReplace(tmpSharePath)
file.writeText(GSON.toJson(sources))
val fileUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileProvider",
file
)
val fileUri = FileProvider.getUriForFile(context, AppConst.authority, file)
intent.type = "text/*"
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

@ -12,6 +12,7 @@ import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ImageLoader
import io.legado.app.utils.getPrefString
import splitties.init.appCtx
@ -132,34 +133,40 @@ class CoverImageView @JvmOverloads constructor(
fun load(path: String?, name: String?, author: String?) {
setText(name, author)
ImageLoader.load(context, path)//Glide自动识别http://,content://和file://
.placeholder(defaultDrawable)
.error(defaultDrawable)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
loadFailed = true
return false
}
if (AppConfig.useDefaultCover) {
ImageLoader.load(context, defaultDrawable)
.centerCrop()
.into(this)
} else {
ImageLoader.load(context, path)//Glide自动识别http://,content://和file://
.placeholder(defaultDrawable)
.error(defaultDrawable)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
loadFailed = true
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
loadFailed = false
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
loadFailed = false
return false
}
})
.centerCrop()
.into(this)
})
.centerCrop()
.into(this)
}
}
companion object {

@ -0,0 +1,48 @@
package io.legado.app.ui.widget.recycler
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
class RecyclerViewAtPager2 : RecyclerView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var startX = 0
private var startY = 0
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
startX = ev.x.toInt()
startY = ev.y.toInt()
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
val endX = ev.x.toInt()
val endY = ev.y.toInt()
val disX = abs(endX - startX)
val disY = abs(endY - startY)
if (disX > disY) {
if (disX > 50) {
parent.requestDisallowInterceptTouchEvent(false)
}
} else {
parent.requestDisallowInterceptTouchEvent(true)
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false)
}
return super.dispatchTouchEvent(ev)
}
}

@ -17,23 +17,30 @@ class UpLinearLayoutManager(val context: Context) : LinearLayoutManager(context)
scroller.offset = offset
startSmoothScroll(scroller)
}
}
class UpLinearSmoothScroller(context: Context?): LinearSmoothScroller(context) {
var offset = 0
class UpLinearSmoothScroller(context: Context?) : LinearSmoothScroller(context) {
var offset = 0
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
override fun getHorizontalSnapPreference(): Int {
return SNAP_TO_START
}
override fun getHorizontalSnapPreference(): Int {
return SNAP_TO_START
}
override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int {
if (snapPreference == SNAP_TO_START) {
return boxStart - viewStart + offset
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
if (snapPreference == SNAP_TO_START) {
return boxStart - viewStart + offset
}
throw IllegalArgumentException("snap preference should be SNAP_TO_START")
}
throw IllegalArgumentException("snap preference should be SNAP_TO_START")
}
}
}

@ -6,25 +6,50 @@ import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.dp
import io.legado.app.utils.getCompatColor
class AccentStrokeTextView(context: Context, attrs: AttributeSet) :
AppCompatTextView(context, attrs) {
private var radius = 3.dp
private val isBottomBackground: Boolean
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentStrokeTextView)
radius = typedArray.getDimensionPixelOffset(R.styleable.StrokeTextView_radius, radius)
isBottomBackground =
typedArray.getBoolean(R.styleable.StrokeTextView_isBottomBackground, false)
typedArray.recycle()
upStyle()
}
private fun upStyle() {
val isLight = ColorUtils.isColorLight(context.bottomBackground)
val disableColor = if (isBottomBackground) {
if (isLight) {
context.getCompatColor(R.color.md_light_disabled)
} else {
context.getCompatColor(R.color.md_dark_disabled)
}
} else {
context.getCompatColor(R.color.disabled)
}
background = Selector.shapeBuild()
.setCornerRadius(3.dp)
.setCornerRadius(radius)
.setStrokeWidth(1.dp)
.setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500))
.setDisabledStrokeColor(disableColor)
.setDefaultStrokeColor(ThemeStore.accentColor(context))
.setPressedBgColor(context.getCompatColor(R.color.transparent30))
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(ThemeStore.accentColor(context))
.setDisabledColor(context.getCompatColor(R.color.md_grey_500))
.setDisabledColor(disableColor)
.create()
)
}
}

@ -21,8 +21,8 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.constant.AppConst
import java.io.File
import java.io.FileOutputStream
@ -158,11 +158,7 @@ fun Context.shareWithQr(text: String, title: String = getString(R.string.share))
fOut.flush()
fOut.close()
file.setReadable(true, false)
val contentUri = FileProvider.getUriForFile(
this,
"${BuildConfig.APPLICATION_ID}.fileProvider",
file
)
val contentUri = FileProvider.getUriForFile(this, AppConst.authority, file)
val intent = Intent(Intent.ACTION_SEND)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(Intent.EXTRA_STREAM, contentUri)

@ -1,37 +1,30 @@
package io.legado.app.utils
import io.legado.app.constant.AppPattern
import io.legado.app.model.analyzeRule.AnalyzeUrl
import java.net.URL
import java.util.regex.Pattern
object HtmlFormatter {
private val wrapHtmlRegex = "</?(?:div|p|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex()
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val imgPattern = Pattern.compile("<img [^>]*src=.*?\"(.*?(?:,\\{.*\\})?)\".*?>")
fun format(html: String?): String {
fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String {
html ?: return ""
return html.replace(wrapHtmlRegex, "\n")
.replace(otherHtmlRegex, "")
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")
.replace(otherRegex, "")
.replace("^[\\n\\s]*".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "")
}
fun formatKeepImg(html: String?): String {
html ?: return ""
return html.replace(wrapHtmlRegex, "\n")
.replace(notImgHtmlRegex, "")
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "")
}
fun formatKeepImg(html: String?) = format(html, notImgHtmlRegex)
fun formatKeepImg(html: String?, redirectUrl: URL?): String {
html ?: return ""
val keepImgHtml = formatKeepImg(html)
val sb = StringBuffer()
val matcher = imgPattern.matcher(html)
val matcher = AppPattern.imgPattern.matcher(keepImgHtml)
var appendPos = 0
while (matcher.find()) {
val urlArray = matcher.group(1)!!.split(AnalyzeUrl.splitUrlRegex)
@ -39,18 +32,14 @@ object HtmlFormatter {
if (urlArray.size > 1) {
url = "$url,${urlArray[1]}"
}
sb.append(html.substring(appendPos, matcher.start()))
sb.append(keepImgHtml.substring(appendPos, matcher.start()))
sb.append("<img src=\"$url\" >")
appendPos = matcher.end()
}
if (appendPos < html.length) {
sb.append(html.substring(appendPos, html.length))
if (appendPos < keepImgHtml.length) {
sb.append(keepImgHtml.substring(appendPos, keepImgHtml.length))
}
return sb.replace(wrapHtmlRegex, "\n")
.replace(notImgHtmlRegex, "")
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "")
return sb.toString()
}
}
}

@ -3,7 +3,7 @@ package io.legado.app.web
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
import io.legado.app.api.ReturnData
import io.legado.app.api.controller.BookshelfController
import io.legado.app.api.controller.BookController
import io.legado.app.api.controller.SourceController
import io.legado.app.web.utils.AssetsWeb
import java.util.*
@ -32,25 +32,26 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
session.parseBody(files)
val postData = files["postData"]
when (uri) {
"/saveSource" -> returnData = SourceController.saveSource(postData)
"/saveSources" -> returnData = SourceController.saveSources(postData)
"/saveBook" -> returnData = BookshelfController.saveBook(postData)
"/deleteSources" -> returnData = SourceController.deleteSources(postData)
returnData = when (uri) {
"/saveSource" -> SourceController.saveSource(postData)
"/saveSources" -> SourceController.saveSources(postData)
"/saveBook" -> BookController.saveBook(postData)
"/deleteSources" -> SourceController.deleteSources(postData)
else -> null
}
}
"GET" -> {
val parameters = session.parameters
when (uri) {
"/getSource" -> returnData = SourceController.getSource(parameters)
"/getSources" -> returnData = SourceController.sources
"/getBookshelf" -> returnData = BookshelfController.bookshelf
"/getChapterList" ->
returnData = BookshelfController.getChapterList(parameters)
"/getBookContent" ->
returnData = BookshelfController.getBookContent(parameters)
returnData = when (uri) {
"/getSource" -> SourceController.getSource(parameters)
"/getSources" -> SourceController.sources
"/getBookshelf" -> BookController.bookshelf
"/getChapterList" -> BookController.getChapterList(parameters)
"/refreshToc" -> BookController.refreshToc(parameters)
"/getBookContent" -> BookController.getBookContent(parameters)
else -> null
}
}
}

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@color/ate_button_disabled_light" />
<item android:state_enabled="false" android:color="@color/disabled" />
<item android:state_pressed="true" android:color="@color/btn_bg_press" />
<item android:state_focused="true" android:color="@color/btn_bg_press_2" />
<item android:color="@color/primaryText" />

@ -31,7 +31,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/find" />
android:text="@string/discovery" />
<TextView
android:layout_width="wrap_content"

@ -12,7 +12,7 @@
app:contentInsetStartWithNavigation="0dp"
app:contentLayout="@layout/view_tab_layout" />
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

@ -12,7 +12,7 @@
android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/find" />
app:title="@string/discovery" />
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:id="@+id/content_view"

@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager_main"
android:layout_width="match_parent"
android:layout_height="0dp"

@ -16,7 +16,7 @@
android:layout_height="wrap_content"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

@ -14,6 +14,27 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<io.legado.app.ui.widget.text.AccentTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/group_style"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/sp_group_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/group_style"
app:theme="@style/Spinner" />
</LinearLayout>
<io.legado.app.ui.widget.text.AccentTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -55,13 +76,24 @@
</LinearLayout>
<io.legado.app.lib.theme.view.ATESwitch
android:id="@+id/sw_show_unread"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/show_unread"
app:layout_constraintLeft_toRightOf="@+id/ll_layout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<LinearLayout
android:id="@+id/ll_sort"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@+id/ll_layout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/sw_show_unread"
app:layout_constraintRight_toRightOf="parent">
<io.legado.app.ui.widget.text.AccentTextView
@ -101,15 +133,4 @@
</LinearLayout>
<io.legado.app.lib.theme.view.ATESwitch
android:id="@+id/sw_show_unread"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/show_unread"
app:layout_constraintTop_toBottomOf="@+id/ll_layout"
app:layout_constraintLeft_toLeftOf="@+id/ll_layout"
app:layout_constraintRight_toRightOf="@id/ll_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -51,17 +51,44 @@
android:textColor="@color/secondaryText"
android:text="@string/page_key_set_help" />
<TextView
android:id="@+id/tv_ok"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/selector_fillet_btn_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="@string/ok"
android:textColor="@color/primaryText" />
android:orientation="horizontal">
<TextView
android:id="@+id/tv_reset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/selector_fillet_btn_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="@string/reset"
android:textColor="@color/primaryText" />
<Space
android:layout_width="3dp"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_ok"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/selector_fillet_btn_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="@string/ok"
android:textColor="@color/primaryText" />
</LinearLayout>
</LinearLayout>

@ -47,7 +47,7 @@
android:layout_height="wrap_content"
android:padding="3dp"
app:max="10"
app:title="字号" />
app:title="@string/title_font_size" />
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_title_top"
@ -55,7 +55,7 @@
android:layout_height="wrap_content"
android:padding="3dp"
app:max="100"
app:title="上间距" />
app:title="@string/title_margin_top" />
<io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_title_bottom"
@ -63,7 +63,7 @@
android:layout_height="wrap_content"
android:padding="3dp"
app:max="100"
app:title="下间距" />
app:title="@string/title_margin_bottom" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_header_padding"
@ -85,13 +85,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/show_hide" />
android:text="@string/show_hide"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_header_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/show_hide" />
</LinearLayout>
@ -109,13 +111,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/left" />
android:text="@string/left"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_header_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -133,13 +137,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/middle" />
android:text="@string/middle"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_header_middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -157,13 +163,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/right" />
android:text="@string/right"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_header_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -187,13 +195,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/show_hide" />
android:text="@string/show_hide"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_footer_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/show_hide" />
</LinearLayout>
@ -211,13 +221,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/left" />
android:text="@string/left"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_footer_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -235,13 +247,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/middle" />
android:text="@string/middle"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_footer_middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -259,13 +273,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/right" />
android:text="@string/right"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_footer_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/title" />
</LinearLayout>
@ -289,13 +305,15 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/text_color" />
android:text="@string/text_color"
android:textColor="@color/primaryText" />
<TextView
android:id="@+id/tv_tip_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:textColor="@color/primaryText"
tools:text="@string/text_color" />
</LinearLayout>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<io.legado.app.ui.main.bookshelf.RootView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -13,9 +13,9 @@
app:contentLayout="@layout/view_tab_layout_min"
app:title="@string/bookshelf" />
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager_bookshelf"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</io.legado.app.ui.main.bookshelf.RootView>
</LinearLayout>

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:attachToActivity="false"
app:contentLayout="@layout/view_search"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/bookshelf" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toBottomOf="parent">
<io.legado.app.ui.widget.recycler.RecyclerViewAtPager2
android:id="@+id/rv_bookshelf"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_bookshelf_list" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/tv_empty_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/bookshelf_empty"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:text="TextView" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -13,9 +13,9 @@
app:attachToActivity="false"
app:contentLayout="@layout/view_search"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/find" />
app:title="@string/discovery" />
<androidx.recyclerview.widget.RecyclerView
<io.legado.app.ui.widget.recycler.RecyclerViewAtPager2
android:id="@+id/rv_find"
android:layout_width="match_parent"
android:layout_height="0dp"

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

Loading…
Cancel
Save