Merge pull request #2 from gedoor/master

merge
pull/122/head
Celeter 5 years ago committed by GitHub
commit 179bcff88f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/build.gradle
  2. 29
      app/google-services.json
  3. 69
      app/src/debug/google-services.json
  4. 38
      app/src/main/AndroidManifest.xml
  5. 46
      app/src/main/assets/txtTocRule.json
  6. 35
      app/src/main/assets/updateLog.md
  7. 9
      app/src/main/java/io/legado/app/App.kt
  8. 9
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  9. 5
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  10. 4
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  11. 12
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  12. 2
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  13. 8
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  14. 1
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  15. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  16. 12
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  17. 2
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  18. 6
      app/src/main/java/io/legado/app/help/AppConfig.kt
  19. 16
      app/src/main/java/io/legado/app/help/BookHelp.kt
  20. 34
      app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt
  21. 2
      app/src/main/java/io/legado/app/help/ItemTouchCallback.kt
  22. 157
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  23. 10
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  24. 13
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  25. 9
      app/src/main/java/io/legado/app/model/WebBook.kt
  26. 12
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  27. 22
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  28. 23
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  29. 2
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  30. 6
      app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt
  31. 28
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  32. 17
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  33. 16
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  34. 23
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  35. 44
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  36. 38
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  37. 14
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  38. 4
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  39. 4
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  40. 9
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  41. 167
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  42. 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  43. 7
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  44. 173
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  45. 2
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  46. 11
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  47. 14
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  48. 66
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  49. 259
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  50. 311
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  51. 39
      app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt
  52. 583
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  53. 122
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  54. 15
      app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt
  55. 10
      app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt
  56. 156
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  57. 98
      app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt
  58. 37
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  59. 203
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  60. 126
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  61. 81
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  62. 8
      app/src/main/java/io/legado/app/ui/book/read/page/entities/SelectPoint.kt
  63. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt
  64. 24
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  65. 40
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  66. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPoint.kt
  67. 4
      app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt
  68. 24
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  69. 4
      app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt
  70. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  71. 33
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  72. 22
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  73. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  74. 8
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  75. 4
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt
  76. 2
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  77. 4
      app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt
  78. 4
      app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt
  79. 46
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  80. 1
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  81. 4
      app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt
  82. 8
      app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt
  83. 42
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  84. 12
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  85. 4
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  86. 9
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
  87. 8
      app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt
  88. 24
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  89. 10
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt
  90. 6
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt
  91. 4
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt
  92. 4
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt
  93. 23
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  94. 44
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  95. 12
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  96. 8
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  97. 22
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  98. 4
      app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt
  99. 6
      app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt
  100. 2
      app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -54,6 +54,7 @@ android {
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.release'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@ -123,7 +124,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.3'
def room_version = '2.2.4'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
@ -139,7 +140,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.4.5'
//
def coroutines_version = '1.2.2'
def coroutines_version = '1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -175,7 +176,7 @@ dependencies {
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
//
//
implementation 'com.github.houbb:opencc4j:1.4.0'
}

@ -63,6 +63,35 @@
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7",
"android_client_info": {
"package_name": "io.legado.app.release"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"

@ -1,69 +0,0 @@
{
"project_info": {
"project_number": "453392274790",
"firebase_url": "https://legado-fca69.firebaseio.com",
"project_id": "legado-fca69",
"storage_bucket": "legado-fca69.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:1d2b1eefbe0e78cff624a7",
"android_client_info": {
"package_name": "io.legado.app"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7",
"android_client_info": {
"package_name": "io.legado.app.debug"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

@ -123,9 +123,6 @@
<activity
android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" />
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTask" />
<activity
android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" />
@ -135,24 +132,51 @@
<activity
android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" />
<activity android:name=".ui.book.arrange.ArrangeBookActivity" />
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
<!--书源管理-->
<activity android:name=".ui.book.source.manage.BookSourceActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="booksource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--订阅源管理-->
<activity android:name=".ui.rss.source.manage.RssSourceActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="rsssource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--替换规则界面-->
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="replace"
android:scheme="yuedu" />
</intent-filter>
</activity>
<activity android:name=".ui.book.arrange.ArrangeBookActivity" />
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
<activity android:name=".ui.chapterlist.ChapterListActivity" />
<activity android:name=".ui.rss.read.ReadRssActivity" />
<activity android:name=".ui.importbook.ImportBookActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.rss.favorites.RssFavoritesActivity" />

@ -7,50 +7,56 @@
},
{
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "^[ \\t]{0,4}[\\[〈「『〖〔《(【\\(]?[第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[\\.:: \f\t].{0,30}$",
"name": "目录",
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{
"enable": true,
"name": "特殊符号 标题",
"rule": "^[ \\t]{0,4}[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"enable": false,
"name": "目录(不匹配行前空白)",
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 2
},
{
"enable": false,
"name": "特殊符号 标题(不匹配空白字符)",
"rule": "(?<=\\s)[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"name": "目录(去简介)",
"rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 3
},
{
"enable": true,
"name": "Chapter/Section/Part 序号 标题",
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$",
"enable": false,
"name": "目录(古典小说备用)",
"rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 4
},
{
"enable": true,
"name": "正文 标题/序号",
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$",
"name": "Chapter/Section/Part 序号 标题",
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 5
},
{
"enable": true,
"name": "目录",
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"name": "正文 标题/序号",
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$",
"serialNumber": 6
},
{
"enable": false,
"name": "目录(不匹配行前空白,正文不出现双标题)",
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$",
"serialNumber": 7
},
{
"enable": false,
"name": "目录(古典小说备用)",
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$",
"enable": true,
"name": "特殊符号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$",
"serialNumber": 8
},
{
"enable":false,
"name": "特殊符号 标题(不匹配空白字符)",
"rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$",
"serialNumber": 9
}
]

@ -2,6 +2,41 @@
* 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/02/29**
* 添加书源一键导入
* 修复主题模式跟随系统
* 修复书源校验
* 添加书架排序
**2020/02/28**
* 解决阅读界面部分字体超出范围的问题
* 修复背景切换有时空白的bug
* 修复滚动翻页问题
**2020/02/27**
* 修复bug,边距调节,换源等一些bug,记不清了
* 修复默认字体问题
* 改了下包名,好上架应用市场
**2020/02/26**
* 修复仿真翻页
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by hingbong
**2020/02/25**
* 优化文本选择和滚动,感觉很完美了
**2020/02/24**
* 滚动暂时可以滚了,先这样吧,头大
* 紧急修复朗读报错的bug
**2020/02/23**
* 修复BUG
* 本地目录正则自定义完成
* 选择文本修复框选不全的问题,增加操作按钮
**2020/02/22**
* 长按选择完成
**2020/02/21**
* 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整
* 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题

@ -5,6 +5,7 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
@ -62,6 +63,13 @@ class App : Application() {
registerActivityLife()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES, Configuration.UI_MODE_NIGHT_NO -> applyDayNight()
}
}
/**
* 更新主题
*/
@ -91,7 +99,6 @@ class App : Application() {
)
.apply()
}
// ChapterProvider.upReadAloudSpan()
}
fun applyDayNight() {

@ -2,6 +2,7 @@ package io.legado.app.base
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -21,4 +22,12 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
super.onDestroy()
job.cancel()
}
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
}

@ -293,6 +293,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
fun getItem(position: Int): ITEM? =
if (position in 0 until items.size) items[position] else null
fun getItemByLayoutPosition(position: Int): ITEM? {
val pos = position - getHeaderCount()
return if (pos in 0 until items.size) items[pos] else null
}
fun getItems(): List<ITEM> = items
protected open fun getItemViewType(item: ITEM, position: Int): Int {

@ -16,6 +16,7 @@ object PreferKey {
const val nextKey = "nextKeyCode"
const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort"
const val recordLog = "recordLog"
const val processText = "process_text"
const val cleanCache = "cleanCache"
@ -36,4 +37,7 @@ object PreferKey {
const val textSelectAble = "selectText"
const val lastBackup = "lastBackup"
const val bodyIndent = "textIndent"
const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces"
}

@ -14,19 +14,19 @@ interface BookSourceDao {
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun liveExplore(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
fun liveExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
@Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''")
@Query("select bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
fun liveGroupEnabled(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and bookSourceGroup is not null and bookSourceGroup <> ''")
@Query("select bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''")
fun liveGroupExplore(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
@ -41,7 +41,7 @@ interface BookSourceDao {
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource>
@get:Query("select * from book_sources where bookUrlPattern is not null || bookUrlPattern <> ''")
@get:Query("select * from book_sources where trim(bookUrlPattern) <> ''")
val hasBookUrlPattern: List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")

@ -28,7 +28,7 @@ interface RssSourceDao {
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun liveEnabled(): LiveData<List<RssSource>>
@Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
@Query("select sourceGroup from rssSources where trim(sourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>>
@get:Query("select min(customOrder) from rssSources")

@ -1,11 +1,15 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.TxtTocRule
@Dao
interface TxtTocRuleDao {
@Query("select * from txtTocRules order by serialNumber")
fun observeAll(): LiveData<List<TxtTocRule>>
@get:Query("select * from txtTocRules order by serialNumber")
val all: List<TxtTocRule>
@ -15,6 +19,10 @@ interface TxtTocRuleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rule: TxtTocRule)
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg rule: TxtTocRule)
@Delete
fun delete(vararg rule: TxtTocRule)
}

@ -3,6 +3,7 @@ package io.legado.app.data.entities
import io.legado.app.utils.splitNotBlank
interface BaseBook {
var bookUrl: String
var variableMap: HashMap<String, String>?
var kind: String?
var wordCount: String?

@ -17,7 +17,7 @@ import kotlin.math.max
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
data class Book(
@PrimaryKey
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 or 本地书籍文件名

@ -21,7 +21,7 @@ import kotlinx.android.parcel.Parcelize
)
data class SearchBook(
@PrimaryKey
var bookUrl: String = "",
override var bookUrl: String = "",
var origin: String = "", // 书源规则
var originName: String = "",
var type: Int = 0, // @BookType
@ -78,16 +78,12 @@ data class SearchBook(
variable = GSON.toJson(variableMap)
}
@Ignore
@delegate:Ignore
@IgnoredOnParcel
var origins: LinkedHashSet<String>? = null
private set
val origins: LinkedHashSet<String> by lazy { linkedSetOf(origin) }
fun addOrigin(origin: String) {
if (origins == null) {
origins = linkedSetOf(this.origin)
}
origins?.add(origin)
origins.add(origin)
}
fun getDisplayLastChapterTitle(): String {

@ -9,6 +9,6 @@ data class TxtTocRule(
@PrimaryKey
var name: String = "",
var rule: String = "",
var serialNumber: Int,
var serialNumber: Int = -1,
var enable: Boolean = true
)

@ -86,4 +86,10 @@ object AppConfig {
set(value) {
App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value)
}
var systemTypefaces: Int
get() = App.INSTANCE.getPrefInt(PreferKey.systemTypefaces)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value)
}
}

@ -237,8 +237,14 @@ object BookHelp {
private var bookName: String? = null
private var bookOrigin: String? = null
private var replaceRules: List<ReplaceRule> = arrayListOf()
val bodyIndent
get() = " ".repeat(App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2))
var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
set(value) {
field = value
App.INSTANCE.putPrefInt(PreferKey.bodyIndent, value)
bodyIndent = " ".repeat(value)
}
var bodyIndent = " ".repeat(bodyIndentCount)
fun disposeContent(
title: String,
@ -247,7 +253,6 @@ object BookHelp {
content: String,
enableReplace: Boolean
): String {
var c = content
synchronized(this) {
if (enableReplace && (bookName != name || bookOrigin != origin)) {
replaceRules = if (origin.isNullOrEmpty()) {
@ -257,6 +262,7 @@ object BookHelp {
}
}
}
var c = content
for (item in replaceRules) {
item.pattern.let {
if (it.isNotEmpty()) {
@ -268,8 +274,8 @@ object BookHelp {
}
}
}
if (!content.substringBefore("\n").contains(title)) {
c = title + "\n" + c
if (!c.substringBefore("\n").contains(title)) {
c = "$title\n$c"
}
when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(c)

@ -1,34 +0,0 @@
package io.legado.app.help
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.base.adapter.ItemViewHolder
class FirstTopListUpCallback : ListUpdateCallback {
var firstInsert = -1
lateinit var adapter: RecyclerView.Adapter<ItemViewHolder>
override fun onChanged(position: Int, count: Int, payload: Any?) {
adapter.notifyItemRangeChanged(position, count, payload)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
if (toPosition == 0) {
firstInsert = 0
}
adapter.notifyItemMoved(fromPosition, toPosition)
}
override fun onInserted(position: Int, count: Int) {
if (firstInsert == -1 || firstInsert > position) {
firstInsert = position
}
adapter.notifyItemRangeInserted(position, count)
}
override fun onRemoved(position: Int, count: Int) {
adapter.notifyItemRangeRemoved(position, count)
}
}

@ -128,7 +128,7 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
* @return 开发者处理了操作应该返回true开发者没有处理就返回false
*/
fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
return false
return true
}
fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {

@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.*
@ -23,12 +24,25 @@ object ReadBookConfig {
val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes())
GSON.fromJsonArray<Config>(json)!!
}
val durConfig
get() = getConfig(styleSelect)
var styleSelect
get() = App.INSTANCE.getPrefInt("readStyleSelect")
set(value) = App.INSTANCE.putPrefInt("readStyleSelect", value)
val durConfig get() = getConfig(styleSelect)
private val shareConfig get() = getConfig(5)
var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
set(value) {
field = value
App.INSTANCE.putPrefInt(PreferKey.readStyleSelect, value)
}
var shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout)
set(value) {
field = value
App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value)
}
var pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim)
set(value) {
field = value
isScroll = value == 3
App.INSTANCE.putPrefInt(PreferKey.pageAnim, value)
}
var isScroll = pageAnim == 3
var bg: Drawable? = null
init {
@ -40,6 +54,9 @@ object ReadBookConfig {
if (configList.size < 5) {
resetAll()
}
if (configList.size < 6) {
configList.add(Config())
}
return configList[index]
}
@ -94,20 +111,122 @@ object ReadBookConfig {
}
}
data class Config(
var bgStr: String = "#EEEEEE",//白天背景
var bgStrNight: String = "#000000",//夜间背景
var bgType: Int = 0,//白天背景类型
var bgTypeNight: Int = 0,//夜间背景类型
var darkStatusIcon: Boolean = true,//白天是否暗色状态栏
var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
var textColor: String = "#3E3D3B",//白天文字颜色
var textColorNight: String = "#adadad",//夜间文字颜色
//配置写入读取
var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
var textBold: Boolean
get() = if (shareLayout) shareConfig.textBold else durConfig.textBold
set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value
var textSize: Int
get() = if (shareLayout) shareConfig.textSize else durConfig.textSize
set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value
var letterSpacing: Float
get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing
set(value) =
if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value
var lineSpacingExtra: Int
get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra
set(value) =
if (shareLayout) shareConfig.lineSpacingExtra = value
else durConfig.lineSpacingExtra = value
var paragraphSpacing: Int
get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing
set(value) =
if (shareLayout) shareConfig.paragraphSpacing = value
else durConfig.paragraphSpacing = value
var titleCenter: Boolean
get() = if (shareLayout) shareConfig.titleCenter else durConfig.titleCenter
set(value) =
if (shareLayout) shareConfig.titleCenter = value else durConfig.titleCenter = value
var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom
set(value) =
if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value
var paddingLeft: Int
get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft
set(value) =
if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value
var paddingRight: Int
get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight
set(value) =
if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value
var paddingTop: Int
get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop
set(value) =
if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value
var headerPaddingBottom: Int
get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom
set(value) =
if (shareLayout) shareConfig.headerPaddingBottom = value
else durConfig.headerPaddingBottom = value
var headerPaddingLeft: Int
get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft
set(value) =
if (shareLayout) shareConfig.headerPaddingLeft = value
else durConfig.headerPaddingLeft = value
var headerPaddingRight: Int
get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight
set(value) =
if (shareLayout) shareConfig.headerPaddingRight = value
else durConfig.headerPaddingRight = value
var headerPaddingTop: Int
get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop
set(value) =
if (shareLayout) shareConfig.headerPaddingTop = value
else durConfig.headerPaddingTop = value
var footerPaddingBottom: Int
get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom
set(value) =
if (shareLayout) shareConfig.footerPaddingBottom = value
else durConfig.footerPaddingBottom = value
var footerPaddingLeft: Int
get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft
set(value) =
if (shareLayout) shareConfig.footerPaddingLeft = value
else durConfig.footerPaddingLeft = value
var footerPaddingRight: Int
get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight
set(value) =
if (shareLayout) shareConfig.footerPaddingRight = value
else durConfig.footerPaddingRight = value
var footerPaddingTop: Int
get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop
set(value) =
if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value
class Config(
private var bgStr: String = "#EEEEEE",//白天背景
private var bgStrNight: String = "#000000",//夜间背景
private var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片
private var bgTypeNight: Int = 0,//夜间背景类型
private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏
private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
private var textColor: String = "#3E3D3B",//白天文字颜色
private var textColorNight: String = "#ADADAD",//夜间文字颜色
var textBold: Boolean = false,//是否粗体字
var textSize: Int = 15,//文字大小
var textSize: Int = 20,//文字大小
var letterSpacing: Float = 1f,//字间距
var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,
var paragraphSpacing: Int = 12,//段距
var titleCenter: Boolean = true,//标题居中
var paddingBottom: Int = 6,
var paddingLeft: Int = 16,
var paddingRight: Int = 16,
@ -137,7 +256,7 @@ object ReadBookConfig {
} else {
textColor = "#${color.hexString}"
}
ChapterProvider.upStyle(this)
ChapterProvider.upStyle()
}
fun setStatusIconDark(isDark: Boolean) {
@ -180,7 +299,7 @@ object ReadBookConfig {
1 -> {
BitmapDrawable(
resources,
BitmapUtils.decodeBitmap(
BitmapUtils.decodeAssetsBitmap(
App.INSTANCE,
"bg" + File.separator + bgStr(),
width,

@ -100,13 +100,9 @@ object Backup {
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName)
doc?.let {
DocumentUtils.writeText(
context,
file.readText(),
doc.uri
)
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)?.let {
DocumentUtils.writeBytes(context, file.readBytes(), it.uri)
}
}
}

@ -12,6 +12,8 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.*
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
@ -104,8 +106,17 @@ object Restore {
else -> Unit
}
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
edit.commit()
edit.apply()
}
ReadBookConfig.apply {
styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout)
pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim)
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
}
ChapterProvider.upStyle()
ReadBook.loadContent()
}
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
}

@ -86,7 +86,8 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<Book> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
val body = if (!book.infoHtml.isNullOrEmpty()) {
val body =
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml
} else {
val analyzeUrl = AnalyzeUrl(
@ -112,7 +113,8 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
val body = if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
val body =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
AnalyzeUrl(
@ -141,7 +143,8 @@ class WebBook(val bookSource: BookSource) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
return@async bookChapter.url
}
val body = if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
val analyzeUrl =

@ -205,6 +205,9 @@ object AnalyzeTxtFile {
}
private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? {
if (book.tocUrl.isNotEmpty()) {
return Pattern.compile(book.tocUrl, Pattern.MULTILINE)
}
val tocRules = getTocRules()
var rulePattern: Pattern? = null
//首先获取128k的数据
@ -227,13 +230,18 @@ object AnalyzeTxtFile {
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
return getDefaultRules()
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
}
}
}
return rules
return emptyList()
}
}

@ -31,6 +31,7 @@ object BookChapterList {
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val analyzeRule = AnalyzeRule(book)
val tocRule = bookSource.getTocRule()
val nextUrlList = arrayListOf(baseUrl)
var reverse = false
@ -43,7 +44,10 @@ object BookChapterList {
listRule = listRule.substring(1)
}
var chapterData =
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true)
analyzeChapterList(
analyzeRule.setContent(body, baseUrl),
book.bookUrl, baseUrl, tocRule, listRule, bookSource, log = true
)
chapterData.chapterList?.let {
chapterList.addAll(it)
}
@ -63,8 +67,8 @@ object BookChapterList {
).getResponseAwait()
.body?.let { nextBody ->
chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule,
book, bookSource, log = false
analyzeRule.setContent(nextBody, nextUrl),
book.bookUrl, nextUrl, tocRule, listRule, bookSource
)
nextUrl = if (chapterData.nextUrl.isNotEmpty()) {
chapterData.nextUrl[0]
@ -98,7 +102,9 @@ object BookChapterList {
headerMapF = bookSource.getHeaderMap()
).getResponseAwait().body
val nextChapterData = analyzeChapterList(
nextBody, item.nextUrl, tocRule, listRule, book, bookSource
analyzeRule.setContent(nextBody, item.nextUrl),
book.bookUrl, item.nextUrl, tocRule, listRule, bookSource,
false
)
synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish(
@ -167,19 +173,17 @@ object BookChapterList {
}
private fun analyzeChapterList(
body: String?,
analyzeRule: AnalyzeRule,
bookUrl: String,
baseUrl: String,
tocRule: TocRule,
listRule: String,
book: Book,
bookSource: BookSource,
getNextUrl: Boolean = true,
log: Boolean = false
): ChapterData<List<String>> {
val chapterList = arrayListOf<BookChapter>()
val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log)
@ -208,7 +212,7 @@ object BookChapterList {
var isVip: String?
for (item in elements) {
analyzeRule.setContent(item)
val bookChapter = BookChapter(bookUrl = book.bookUrl)
val bookChapter = BookChapter(bookUrl = bookUrl)
analyzeRule.chapter = bookChapter
bookChapter.title = analyzeRule.getString(nameRule)
bookChapter.url = analyzeRule.getString(urlRule, true)

@ -33,11 +33,15 @@ object BookContent {
)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val analyzeRule = AnalyzeRule(book)
val content = StringBuilder()
val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule()
var contentData = analyzeContent(body, contentRule, book, bookChapter, bookSource, baseUrl)
content.append(contentData.content)
var contentData = analyzeContent(
analyzeRule.setContent(body, baseUrl),
contentRule, bookChapter, bookSource
)
content.append(contentData.content.replace(bookChapter.title, ""))
if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
@ -58,8 +62,8 @@ object BookContent {
.body?.let { nextBody ->
contentData =
analyzeContent(
nextBody, contentRule, book,
bookChapter, bookSource, baseUrl, false
analyzeRule.setContent(nextBody, nextUrl),
contentRule, bookChapter, bookSource, false
)
nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
@ -83,8 +87,8 @@ object BookContent {
.body?.let {
contentData =
analyzeContent(
it, contentRule, book, bookChapter,
bookSource, item.nextUrl, false
analyzeRule.setContent(it, item.nextUrl),
contentRule, bookChapter, bookSource, false
)
item.content = contentData.content
}
@ -94,6 +98,7 @@ object BookContent {
content.append(item.content)
}
}
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")
@ -103,17 +108,13 @@ object BookContent {
@Throws(Exception::class)
private fun analyzeContent(
body: String,
analyzeRule: AnalyzeRule,
contentRule: ContentRule,
book: Book,
chapter: BookChapter,
bookSource: BookSource,
baseUrl: String,
printLog: Boolean = true
): ContentData<List<String>> {
val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.chapter = chapter
val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) {

@ -45,7 +45,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
}
}
}
return false
return true
}
private fun readAloud(context: Context) {

@ -9,12 +9,12 @@ import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent
class TimeElectricityReceiver : BroadcastReceiver() {
class TimeBatteryReceiver : BroadcastReceiver() {
companion object {
fun register(context: Context): TimeElectricityReceiver {
val receiver = TimeElectricityReceiver()
fun register(context: Context): TimeBatteryReceiver {
val receiver = TimeBatteryReceiver()
val filter = IntentFilter()
filter.addAction(Intent.ACTION_TIME_TICK)
filter.addAction(Intent.ACTION_BATTERY_CHANGED)

@ -14,14 +14,15 @@ import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors
class CheckSourceService : BaseService() {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var idsCount = 0
private val unCheckIds = LinkedHashSet<String>()
private val allIds = LinkedHashSet<String>()
private val checkedIds = LinkedHashSet<String>()
override fun onCreate() {
super.onCreate()
@ -46,12 +47,12 @@ class CheckSourceService : BaseService() {
private fun check(ids: List<String>) {
task?.cancel()
unCheckIds.clear()
idsCount = ids.size
unCheckIds.addAll(ids)
updateNotification(0, getString(R.string.progress_show, 0, idsCount))
task = execute {
unCheckIds.forEach { sourceUrl ->
allIds.clear()
checkedIds.clear()
allIds.addAll(ids)
updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
task = execute(context = searchPool) {
allIds.forEach { sourceUrl ->
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
@ -59,15 +60,16 @@ class CheckSourceService : BaseService() {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally {
unCheckIds.remove(sourceUrl)
val checkedCount = idsCount - unCheckIds.size
checkedIds.add(sourceUrl)
updateNotification(
checkedCount,
getString(R.string.progress_show, checkedCount, idsCount)
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
}
}
}
}.onError {
toast("校验书源出错:${it.localizedMessage}")
}
task?.invokeOnCompletion {
@ -92,7 +94,7 @@ class CheckSourceService : BaseService() {
getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
)
builder.setProgress(idsCount, state, false)
builder.setProgress(allIds.size, state, false)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
startForeground(112202, notification)

@ -5,16 +5,15 @@ import android.media.MediaPlayer
import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
import java.io.File
import java.io.FileDescriptor
import java.io.FileInputStream
@ -27,7 +26,7 @@ class HttpReadAloudService : BaseReadAloudService(),
private val mediaPlayer = MediaPlayer()
private lateinit var ttsFolder: String
private var job: Job? = null
private var task: Coroutine<*>? = null
private var playingIndex = -1
override fun onCreate() {
@ -40,13 +39,12 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onDestroy() {
super.onDestroy()
job?.cancel()
task?.cancel()
mediaPlayer.release()
}
override fun newReadAloud(dataKey: String?, play: Boolean) {
mediaPlayer.reset()
job?.cancel()
playingIndex = -1
super.newReadAloud(dataKey, play)
}
@ -64,7 +62,8 @@ class HttpReadAloudService : BaseReadAloudService(),
}
private fun downloadAudio() {
job = launch(IO) {
task?.cancel()
task = execute {
FileUtils.deleteFile(ttsFolder)
for (index in 0 until contentList.size) {
if (isActive) {
@ -85,6 +84,8 @@ class HttpReadAloudService : BaseReadAloudService(),
break
}
}
}.onError {
toast("下载朗读文件出错:${it.localizedMessage}")
}
}
@ -150,7 +151,7 @@ class HttpReadAloudService : BaseReadAloudService(),
* 更新朗读速度
*/
override fun upSpeechRate(reset: Boolean) {
job?.cancel()
task?.cancel()
mediaPlayer.stop()
playingIndex = -1
downloadAudio()

@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
object ReadBook {
@ -25,10 +26,10 @@ object ReadBook {
var book: Book? = null
var inBookshelf = false
var chapterSize = 0
var callBack: CallBack? = null
var durChapterIndex = 0
var durPageIndex = 0
var isLocalBook = true
var callBack: CallBack? = null
var prevTextChapter: TextChapter? = null
var curTextChapter: TextChapter? = null
var nextTextChapter: TextChapter? = null
@ -126,7 +127,13 @@ object ReadBook {
saveRead()
}
fun curPageChanged() {
fun setPageIndex(pageIndex: Int) {
durPageIndex = pageIndex
saveRead()
curPageChanged()
}
private fun curPageChanged() {
callBack?.upPageProgress()
if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause)
@ -283,6 +290,9 @@ object ReadBook {
}
}
}
}.onError {
it.printStackTrace()
App.INSTANCE.toast(it.localizedMessage ?: "ChapterProvider ERROR")
}
}
@ -302,7 +312,7 @@ object ReadBook {
}
interface CallBack {
fun upContent(position: Int = 0)
fun upContent(relativePosition: Int = 0)
fun upView()
fun upPageProgress()
fun contentLoadFinish()

@ -1,14 +1,12 @@
package io.legado.app.ui.about
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.utils.shareText
import org.jetbrains.anko.toast
import io.legado.app.utils.openUrl
import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) {
@ -28,22 +26,13 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_scoring -> openIntent("market://details?id=$packageName")
R.id.menu_share_it -> shareText(
"App Share",
getString(R.string.app_share_description)
R.id.menu_scoring -> openUrl("market://details?id=$packageName")
R.id.menu_share_it -> share(
getString(R.string.app_share_description),
getString(R.string.app_name)
)
}
return super.onCompatOptionsItemSelected(item)
}
private fun openIntent(address: String) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(address)
startActivity(intent)
} catch (e: Exception) {
toast(R.string.can_not_open)
}
}
}

@ -1,8 +1,5 @@
package io.legado.app.ui.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@ -14,6 +11,8 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
class AboutFragment : PreferenceFragmentCompat() {
@ -43,32 +42,32 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
"contributors" -> openIntent(Intent.ACTION_VIEW, R.string.contributors_url)
"contributors" -> openUrl(R.string.contributors_url)
"update_log" -> showUpdateLog()
"check_update" -> openIntent(Intent.ACTION_VIEW, R.string.latest_release_url)
"mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com")
"git" -> openIntent(Intent.ACTION_VIEW, R.string.this_github_url)
"home_page" -> openIntent(Intent.ACTION_VIEW, R.string.home_page_url)
"license" -> openIntent(Intent.ACTION_VIEW, licenseUrl)
"disclaimer" -> openIntent(Intent.ACTION_VIEW, disclaimerUrl)
"check_update" -> openUrl(R.string.latest_release_url)
"mail" -> sendMail()
"git" -> openUrl(R.string.this_github_url)
"home_page" -> openUrl(R.string.home_page_url)
"license" -> requireContext().openUrl(licenseUrl)
"disclaimer" -> requireContext().openUrl(disclaimerUrl)
"qq" -> showQqGroups()
"gzGzh" -> sendToClip("开源阅读软件")
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
}
return super.onPreferenceTreeClick(preference)
}
@Suppress("SameParameterValue")
private fun openIntent(intentName: String, @StringRes addressID: Int) {
openIntent(intentName, getString(addressID))
private fun openUrl(@StringRes addressID: Int) {
requireContext().openUrl(getString(addressID))
}
private fun openIntent(intentName: String, address: String) {
private fun sendMail() {
try {
val intent = Intent(intentName)
intent.data = Uri.parse(address)
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:kunfei.ge@gmail.com")
startActivity(intent)
} catch (e: Exception) {
toast(R.string.can_not_open)
toast(e.localizedMessage ?: "Error")
}
}
@ -86,7 +85,7 @@ class AboutFragment : PreferenceFragmentCompat() {
items(names) { _, index ->
qqGroups[names[index]]?.let {
if (!joinQQGroup(it)) {
sendToClip(it)
requireContext().sendToClip(it)
}
}
}
@ -107,13 +106,4 @@ class AboutFragment : PreferenceFragmentCompat() {
}
}
private fun sendToClip(text: String) {
val clipboard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
val clipData = ClipData.newPlainText(null, text)
clipboard?.let {
clipboard.setPrimaryClip(clipData)
toast(R.string.copy_complete)
}
}
}

@ -1,7 +1,5 @@
package io.legado.app.ui.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -11,7 +9,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R
import io.legado.app.utils.ACache
import io.legado.app.utils.toast
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip
import org.jetbrains.anko.longToast
import java.net.URLEncoder
@ -33,40 +32,19 @@ class DonateFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
"wxZsm" -> openIntent(Intent.ACTION_VIEW, wxZsRwmUrl)
"zfbHbRwm" -> openIntent(Intent.ACTION_VIEW, zfbHbRwmUrl)
"zfbSkRwm" -> openIntent(Intent.ACTION_VIEW, zfbSkRwmUrl)
"qqSkRwm" -> openIntent(Intent.ACTION_VIEW, qqSkRwmUrl)
"wxZsm" -> requireContext().openUrl(wxZsRwmUrl)
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> sendToClip("开源阅读软件")
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
}
return super.onPreferenceTreeClick(preference)
}
@Suppress("SameParameterValue")
private fun openIntent(intentName: String, address: String) {
try {
val intent = Intent(intentName)
intent.data = Uri.parse(address)
startActivity(intent)
} catch (e: Exception) {
toast(R.string.can_not_open)
}
}
private fun sendToClip(text: String) {
val clipboard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
val clipData = ClipData.newPlainText(null, text)
clipboard?.let {
clipboard.setPrimaryClip(clipData)
toast(R.string.copy_complete)
}
}
private fun getZfbHb(context: Context) {
sendToClip("537954522")
requireContext().sendToClip("537954522")
context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包")
try {
val packageManager = context.applicationContext.packageManager

@ -19,8 +19,8 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.*
@ -60,7 +60,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter
select_action_bar.setMainActionText(R.string.move_to_group)
@ -95,12 +95,14 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initBookData() {
booksLiveData?.removeObservers(this)
booksLiveData =
if (groupId == -1) {
App.db.bookDao().observeAll()
} else {
App.db.bookDao().observeByGroup(groupId)
when (groupId) {
-1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer {
adapter.selectedBooks.clear()
adapter.setItems(it)
upSelectCount()
})

@ -28,8 +28,8 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
@ -77,7 +77,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
.isChecked = AppConst.bookGroupAudioShow
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult =

@ -28,8 +28,8 @@ import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_book_group_picker.*
@ -98,7 +98,7 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.menu.findItem(R.id.menu_group_audio).isVisible = false
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter

@ -10,7 +10,6 @@ import android.view.View.NO_ID
import android.widget.EditText
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
@ -23,7 +22,6 @@ import io.legado.app.lib.theme.ThemeStore
import io.legado.app.service.help.Download
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_download_choice.view.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
@ -42,15 +40,14 @@ object Help {
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
val hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
if (hideNavigationBar) {
if (ReadBookConfig.hideNavigationBar) {
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}
if (toolBarHide) {
if (App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)) {
if (ReadBookConfig.hideStatusBar) {
flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN
}
if (hideNavigationBar) {
if (ReadBookConfig.hideNavigationBar) {
flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}
}

@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.view.size
@ -24,7 +25,7 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.receiver.TimeElectricityReceiver
import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
@ -32,10 +33,10 @@ import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.read.config.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.TextPageFactory
import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.read.page.entities.SelectPoint
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity
@ -55,27 +56,32 @@ import org.jetbrains.anko.toast
class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read),
View.OnTouchListener,
PageView.CallBack,
TextActionMenu.CallBack,
ContentTextView.CallBack,
ReadMenu.CallBack,
ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack,
ReadBook.CallBack,
TocRegexDialog.CallBack,
ColorPickerDialogListener {
private val requestCodeChapterList = 568
private val requestCodeEditSource = 111
private val requestCodeReplace = 312
private var menu: Menu? = null
private var textActionMenu: TextActionMenu? = null
override val viewModel: ReadBookViewModel
get() = getViewModel(ReadBookViewModel::class.java)
override val isInitFinish: Boolean
get() = viewModel.isInitFinish
override val isInitFinish: Boolean get() = viewModel.isInitFinish
private val mHandler = Handler()
private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) }
private var screenTimeOut: Long = 0
private var timeElectricityReceiver: TimeElectricityReceiver? = null
private var timeBatteryReceiver: TimeBatteryReceiver? = null
override val pageFactory: TextPageFactory get() = page_view.pageFactory
override val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onActivityCreated(savedInstanceState: Bundle?) {
Help.upLayoutInDisplayCutoutMode(window)
@ -98,15 +104,15 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onResume() {
super.onResume()
upSystemUiVisibility()
timeElectricityReceiver = TimeElectricityReceiver.register(this)
timeBatteryReceiver = TimeBatteryReceiver.register(this)
page_view.upTime()
}
override fun onPause() {
super.onPause()
timeElectricityReceiver?.let {
timeBatteryReceiver?.let {
unregisterReceiver(it)
timeElectricityReceiver = null
timeBatteryReceiver = null
}
upSystemUiVisibility()
}
@ -209,6 +215,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_book_info -> ReadBook.book?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl))
}
R.id.menu_toc_regex -> TocRegexDialog.show(
supportFragmentManager,
ReadBook.book?.tocUrl
)
}
return super.onCompatOptionsItemSelected(item)
}
@ -318,14 +328,98 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (event.action) {
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
when (v.id) {
R.id.cursor_left -> page_view.selectStartMove(event.x, event.y)
R.id.cursor_right -> page_view.selectEndMove(event.x, event.y)
R.id.cursor_left -> page_view.curPage.selectStartMove(
event.rawX + cursor_left.width,
event.rawY - cursor_left.height
)
R.id.cursor_right -> page_view.curPage.selectEndMove(
event.rawX - cursor_right.width,
event.rawY - cursor_right.height
)
}
}
}
return true
}
/**
* 更新文字选择开始位置
*/
override fun upSelectedStart(x: Float, y: Float) {
cursor_left.x = x - cursor_left.width
cursor_left.y = y
cursor_left.visible(true)
showTextActionMenu()
}
/**
* 更新文字选择结束位置
*/
override fun upSelectedEnd(x: Float, y: Float) {
cursor_right.x = x
cursor_right.y = y
cursor_right.visible(true)
showTextActionMenu()
}
/**
* 取消文字选择
*/
override fun onCancelSelect() {
cursor_left.invisible()
cursor_right.invisible()
textActionMenu?.dismiss()
}
/**
* 显示文本操作菜单
*/
private fun showTextActionMenu() {
textActionMenu ?: let {
textActionMenu = TextActionMenu(this, this)
}
val x = cursor_left.x.toInt() + cursor_left.width
val y = if (cursor_left.y - statusBarHeight > ReadBookConfig.textSize.dp * 1.5 + 20.dp) {
(page_view.height - cursor_left.y + ReadBookConfig.textSize.dp * 1.5).toInt()
} else {
(page_view.height - cursor_left.y - cursor_left.height - 40.dp).toInt()
}
textActionMenu?.let { popup ->
if (!popup.isShowing) {
popup.showAtLocation(cursor_left, Gravity.BOTTOM or Gravity.START, x, y)
} else {
popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT)
}
}
}
/**
* 当前选择的文本
*/
override val selectedText: String get() = page_view.curPage.selectedText
/**
* 文本选择菜单操作
*/
override fun onMenuItemSelected(itemId: Int): Boolean {
when (itemId) {
R.id.menu_replace -> {
ReplaceEditDialog.show(supportFragmentManager, pattern = selectedText)
return true
}
}
return false
}
/**
* 文本选择菜单操作完成
*/
override fun onMenuActionFinally() {
textActionMenu?.dismiss()
page_view.curPage.cancelSelect()
page_view.pageDelegate?.isTextSelected = false
}
/**
* 音量键翻页
*/
@ -357,12 +451,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
}
override fun upContent(position: Int) {
/**
* 更新内容
*/
override fun upContent(relativePosition: Int) {
launch {
page_view.upContent(position)
page_view.upContent(relativePosition)
}
}
/**
* 更新视图
*/
override fun upView() {
launch {
ReadBook.curTextChapter?.let {
@ -380,12 +480,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
}
/**
* 更新进度条
*/
override fun upPageProgress() {
launch {
seek_read_page.progress = ReadBook.durPageIndex
}
}
/**
* 显示菜单
*/
override fun showMenuBar() {
read_menu.runMenuIn()
}
@ -397,12 +503,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
viewModel.changeTo(book)
}
override fun setPageIndex(pageIndex: Int) {
ReadBook.durPageIndex = pageIndex
ReadBook.saveRead()
ReadBook.curPageChanged()
}
override fun clickCenter() {
if (BaseReadAloudService.isRun) {
showReadAloudDialog()
@ -411,19 +511,16 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
}
override fun selectText(selectPoint: SelectPoint) {
cursor_left.x = selectPoint.startX - cursor_left.width
cursor_left.y = selectPoint.startY
cursor_right.x = selectPoint.endX
cursor_right.y = selectPoint.endY
cursor_left.visible()
cursor_right.visible()
}
/**
* 显示朗读菜单
*/
override fun showReadAloudDialog() {
ReadAloudDialog().show(supportFragmentManager, "readAloud")
}
/**
* 自动翻页
*/
override fun autoPage() {
}
@ -495,6 +592,13 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/
override fun onDialogDismissed(dialogId: Int) = Unit
override fun onTocRegexDialogResult(tocRegex: String) {
ReadBook.book?.let {
it.tocUrl = tocRegex
viewModel.loadChapterList(it)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
@ -528,6 +632,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onDestroy() {
super.onDestroy()
mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss()
page_view.onDestroy()
}
override fun observeLiveBus() {
@ -549,7 +655,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility()
page_view.upBg()
page_view.upStyle()
ChapterProvider.upStyle(ReadBookConfig.durConfig)
if (it) {
ReadBook.loadContent()
} else {
@ -586,7 +691,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upScreenTimeOut()
}
observeEvent<Boolean>(PreferKey.textSelectAble) {
page_view.upSelectAble(it)
page_view.curPage.upSelectAble(it)
}
}
@ -605,7 +710,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
Help.keepScreenOn(window, true)
return
}
val t = screenTimeOut - getScreenOffTime()
val t = screenTimeOut - sysScreenOffTime
if (t > 0) {
mHandler.removeCallbacks(keepScreenRunnable)
Help.keepScreenOn(window, true)

@ -99,6 +99,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
execute {
if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let {
App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = it.size

@ -10,8 +10,8 @@ import android.widget.SeekBar
import androidx.core.view.isVisible
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.buttonDisabledColor
import io.legado.app.service.help.ReadBook
@ -199,9 +199,8 @@ class ReadMenu : FrameLayout {
vw_menu_bg.onClick { runMenuOut() }
vwNavigationBar.layoutParams = vwNavigationBar.layoutParams.apply {
height =
if (context.getPrefBoolean(PreferKey.hideNavigationBar)
&& Help.isNavigationBarExist(activity)
) context.getNavigationBarHeight()
if (ReadBookConfig.hideNavigationBar && Help.isNavigationBarExist(activity))
context.navigationBarHeight
else 0
}
}

@ -0,0 +1,173 @@
package io.legado.app.ui.book.read
import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.view.LayoutInflater
import android.view.Menu
import android.view.ViewGroup
import android.widget.PopupWindow
import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.popup_action_menu.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.share
import org.jetbrains.anko.toast
@SuppressLint("RestrictedApi")
class TextActionMenu(private val context: Context, private val callBack: CallBack) :
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
init {
@SuppressLint("InflateParams")
contentView = LayoutInflater.from(context).inflate(R.layout.popup_action_menu, null)
isTouchable = true
isOutsideTouchable = false
isFocusable = false
initRecyclerView()
}
private fun initRecyclerView() = with(contentView) {
val adapter = Adapter(context)
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
recycler_view.adapter = adapter
val menu = MenuBuilder(context)
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val popupMenu = PopupMenu(context, iv_menu_more)
onInitializeMenu(popupMenu.menu)
if (popupMenu.menu.size > 0) {
iv_menu_more.visible()
popupMenu.setOnMenuItemClickListener { item ->
item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
this@TextActionMenu.dismiss()
true
}
}
iv_menu_more.onClick {
popupMenu.show()
}
}
}
inner class Adapter(context: Context) :
SimpleRecyclerAdapter<MenuItemImpl>(context, R.layout.item_text) {
override fun convert(
holder: ItemViewHolder,
item: MenuItemImpl,
payloads: MutableList<Any>
) {
with(holder.itemView) {
text_view.text = item.title
}
}
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.onClick {
getItem(holder.layoutPosition)?.let {
if (!callBack.onMenuItemSelected(it.itemId)) {
onMenuItemSelected(it)
}
}
}
}
}
private fun onMenuItemSelected(item: MenuItemImpl) {
when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(callBack.selectedText)
R.id.menu_browser -> {
try {
val intent = if (callBack.selectedText.isAbsUrl()) {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(callBack.selectedText)
}
} else {
Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, callBack.selectedText)
}
}
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
context.toast(e.localizedMessage ?: "ERROR")
}
}
}
callBack.onMenuActionFinally()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun createProcessTextIntent(): Intent {
return Intent()
.setAction(Intent.ACTION_PROCESS_TEXT)
.setType("text/plain")
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getSupportedActivities(): List<ResolveInfo> {
return context.packageManager
.queryIntentActivities(createProcessTextIntent(), 0)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent {
return createProcessTextIntent()
.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false)
.setClassName(info.activityInfo.packageName, info.activityInfo.name)
}
/**
* Start with a menu Item order value that is high enough
* so that your "PROCESS_TEXT" menu items appear after the
* standard selection menu items like Cut, Copy, Paste.
*/
@RequiresApi(Build.VERSION_CODES.M)
private fun onInitializeMenu(menu: Menu) {
// Start with a menu Item order value that is high enough
// so that your "PROCESS_TEXT" menu items appear after the
// standard selection menu items like Cut, Copy, Paste.
var menuItemOrder = 100
for (resolveInfo in getSupportedActivities()) {
menu.add(
Menu.NONE, Menu.NONE,
menuItemOrder++, resolveInfo.loadLabel(context.packageManager)
).intent = createProcessTextIntentForResolveInfo(resolveInfo)
}
}
interface CallBack {
val selectedText: String
fun onMenuItemSelected(itemId: Int): Boolean
fun onMenuActionFinally()
}
}

@ -155,7 +155,7 @@ class BgTextConfigDialog : DialogFragment() {
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply {
this.onClick {
getItem(holder.layoutPosition)?.let {
getItemByLayoutPosition(holder.layoutPosition)?.let {
ReadBookConfig.durConfig.setBg(1, it)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)

@ -14,6 +14,7 @@ import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.getPrefBoolean
@ -92,8 +93,14 @@ class MoreConfigDialog : DialogFragment() {
key: String?
) {
when (key) {
PreferKey.hideStatusBar -> postEvent(EventBus.UP_CONFIG, true)
PreferKey.hideNavigationBar -> postEvent(EventBus.UP_CONFIG, true)
PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
postEvent(EventBus.UP_CONFIG, true)
}
PreferKey.hideNavigationBar -> {
ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
postEvent(EventBus.UP_CONFIG, true)
}
PreferKey.keepLight -> postEvent(key, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
}

@ -10,7 +10,9 @@ import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.dp
import io.legado.app.utils.postEvent
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_read_padding.*
class PaddingConfigDialog : DialogFragment() {
@ -49,7 +51,15 @@ class PaddingConfigDialog : DialogFragment() {
ReadBookConfig.save()
}
private fun initData() = with(ReadBookConfig.durConfig) {
private fun initData() = with(ReadBookConfig) {
if (hideStatusBar) {
tv_header_padding.visible()
dsb_header_padding_top.visible()
dsb_header_padding_bottom.visible()
dsb_header_padding_left.visible()
dsb_header_padding_right.visible()
tv_body_padding.setPadding(0, 10.dp, 0, 10.dp)
}
//正文
dsb_padding_top.progress = paddingTop
dsb_padding_bottom.progress = paddingBottom
@ -67,7 +77,7 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_right.progress = footerPaddingRight
}
private fun initView() = with(ReadBookConfig.durConfig) {
private fun initView() = with(ReadBookConfig) {
//正文
dsb_padding_top.onChanged = {
paddingTop = it

@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.ImageLoader
import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor
@ -19,7 +19,10 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.*
import io.legado.app.utils.getIndexById
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefString
import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_read_book_style.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange
@ -28,6 +31,8 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
val callBack = activity as? ReadBookActivity
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
@ -76,7 +81,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
}
private fun initData() {
requireContext().getPrefInt(PreferKey.pageAnim).let {
cb_share_layout.isChecked = ReadBookConfig.shareLayout
ReadBookConfig.pageAnim.let {
if (it >= 0 && it < rg_page_anim.childCount) {
rg_page_anim.check(rg_page_anim[it].id)
}
@ -90,8 +96,15 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
chinese_converter.onChanged {
postEvent(EventBus.UP_CONFIG, true)
}
tv_title_center.onClick {
ReadBookConfig.apply {
titleCenter = !titleCenter
tv_title_center.isSelected = titleCenter
}
postEvent(EventBus.UP_CONFIG, true)
}
tv_text_bold.onClick {
ReadBookConfig.durConfig.apply {
ReadBookConfig.apply {
textBold = !textBold
tv_text_bold.isSelected = textBold
}
@ -105,43 +118,41 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
title = getString(R.string.text_indent),
items = resources.getStringArray(R.array.indent).toList()
) { _, index ->
putPrefInt(PreferKey.bodyIndent, index)
BookHelp.bodyIndentCount = index
postEvent(EventBus.UP_CONFIG, true)
}
}
tv_padding.onClick {
val activity = activity
dismiss()
if (activity is ReadBookActivity) {
activity.showPaddingConfig()
}
callBack?.showPaddingConfig()
}
dsb_text_size.onChanged = {
ReadBookConfig.durConfig.textSize = it + 5
ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true)
}
dsb_text_letter_spacing.onChanged = {
ReadBookConfig.durConfig.letterSpacing = (it - 50) / 100f
ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true)
}
dsb_line_size.onChanged = {
ReadBookConfig.durConfig.lineSpacingExtra = it
ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_paragraph_spacing.onChanged = {
ReadBookConfig.durConfig.paragraphSpacing = it
ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true)
}
rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) {
if (checkedId == rg_page_anim[i].id) {
requireContext().putPrefInt(PreferKey.pageAnim, i)
val activity = activity
if (activity is ReadBookActivity) {
activity.page_view.upPageAnim(i)
rg_page_anim.getIndexById(checkedId).let {
ReadBookConfig.pageAnim = it
callBack?.page_view?.upPageAnim()
}
break
}
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
if (checkBox.isPressed) {
ReadBookConfig.shareLayout = isChecked
upStyle()
postEvent(EventBus.UP_CONFIG, true)
}
}
bg0.onClick { changeBg(0) }
@ -169,15 +180,13 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun showBgTextConfig(index: Int): Boolean {
dismiss()
changeBg(index)
val activity = activity
if (activity is ReadBookActivity) {
activity.showBgTextConfig()
}
callBack?.showBgTextConfig()
return true
}
private fun upStyle() {
ReadBookConfig.durConfig.let {
ReadBookConfig.let {
tv_title_center.isSelected = it.titleCenter
tv_text_bold.isSelected = it.textBold
dsb_text_size.progress = it.textSize - 5
dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50
@ -200,12 +209,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
4 -> bg4
else -> bg0
}
ReadBookConfig.getConfig(i).apply {
when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().into(iv)
else -> iv.setImageDrawable(bgDrawable(100, 150))
}
}
iv.setImageDrawable(ReadBookConfig.getConfig(i).bgDrawable(100, 150))
}
}

@ -0,0 +1,259 @@
package io.legado.app.ui.book.read.config
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.cancelButton
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.okButton
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import kotlinx.android.synthetic.main.dialog_toc_regex.*
import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.*
import kotlinx.android.synthetic.main.item_toc_regex.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.*
class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var adapter: TocRegexAdapter
private var tocRegexLiveData: LiveData<List<TxtTocRule>>? = null
var selectedName: String? = null
private var durRegex: String? = null
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.windowManager?.defaultDisplay?.getMetrics(dm)
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_toc_regex, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
durRegex = arguments?.getString("tocRegex")
tool_bar.setTitle(R.string.txt_toc_regex)
tool_bar.inflateMenu(R.menu.txt_toc_regex)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
initView()
initData()
}
private fun initView() {
adapter = TocRegexAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
tv_cancel.onClick {
dismiss()
}
tv_ok.onClick {
adapter.getItems().forEach { tocRule ->
if (selectedName == tocRule.name) {
val callBack = activity as? CallBack
callBack?.onTocRegexDialogResult(tocRule.rule)
dismiss()
return@onClick
}
}
}
}
private fun initData() {
tocRegexLiveData?.removeObservers(viewLifecycleOwner)
tocRegexLiveData = App.db.txtTocRule().observeAll()
tocRegexLiveData?.observe(viewLifecycleOwner, Observer { tocRules ->
initSelectedName(tocRules)
adapter.setItems(tocRules)
})
}
private fun initSelectedName(tocRules: List<TxtTocRule>) {
if (selectedName == null && durRegex != null) {
tocRules.forEach {
if (durRegex == it.rule) {
selectedName = it.name
return@forEach
}
}
if (selectedName == null) {
selectedName = ""
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_add -> editRule()
R.id.menu_default -> importDefault()
}
return false
}
private fun importDefault() {
launch(IO) {
AnalyzeTxtFile.getDefaultRules().let {
App.db.txtTocRule().insert(*it.toTypedArray())
}
}
}
@SuppressLint("InflateParams")
private fun editRule(rule: TxtTocRule? = null) {
val tocRule = rule?.copy() ?: TxtTocRule()
requireContext().alert(titleResource = R.string.txt_toc_regex) {
var rootView: View? = null
customView {
LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_toc_regex_edit, null).apply {
rootView = this
tv_rule_name.setText(tocRule.name)
tv_rule_regex.setText(tocRule.rule)
}
}
okButton {
rootView?.apply {
tocRule.name = tv_rule_name.text.toString()
tocRule.rule = tv_rule_regex.text.toString()
saveRule(tocRule, rule)
}
}
cancelButton()
}.show().applyTint()
}
private fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) {
launch(IO) {
if (rule.serialNumber < 0) {
rule.serialNumber = adapter.getItems().lastOrNull()?.serialNumber ?: 0 + 1
}
oldRule?.let {
App.db.txtTocRule().delete(oldRule)
}
App.db.txtTocRule().insert(rule)
}
}
inner class TocRegexAdapter(context: Context) :
SimpleRecyclerAdapter<TxtTocRule>(context, R.layout.item_toc_regex),
ItemTouchCallback.OnItemTouchCallbackListener {
override fun convert(holder: ItemViewHolder, item: TxtTocRule, payloads: MutableList<Any>) {
holder.itemView.apply {
if (payloads.isEmpty()) {
rb_regex_name.text = item.name
rb_regex_name.isChecked = item.name == selectedName
swt_enabled.isChecked = item.enable
} else {
rb_regex_name.isChecked = item.name == selectedName
}
}
}
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply {
rb_regex_name.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed && isChecked) {
selectedName = getItem(holder.layoutPosition)?.name
updateItems(0, itemCount - 1, true)
}
}
swt_enabled.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let {
it.enable = isChecked
launch(IO) {
App.db.txtTocRule().update(it)
}
}
}
}
iv_edit.onClick {
editRule(getItem(holder.layoutPosition))
}
iv_delete.onClick {
getItem(holder.layoutPosition)?.let { item ->
launch(IO) {
App.db.txtTocRule().delete(item)
}
}
}
}
}
private var isMoved = false
override fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
Collections.swap(getItems(), srcPosition, targetPosition)
notifyItemMoved(srcPosition, targetPosition)
isMoved = true
return super.onMove(srcPosition, targetPosition)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
if (isMoved) {
for ((index, item) in getItems().withIndex()) {
item.serialNumber = index + 1
}
launch(IO) {
App.db.txtTocRule().update(*getItems().toTypedArray())
}
}
isMoved = false
}
}
companion object {
fun show(fragmentManager: FragmentManager, tocRegex: String? = null) {
val dialog = TocRegexDialog()
val bundle = Bundle()
bundle.putString("tocRegex", tocRegex)
dialog.arguments = bundle
dialog.show(fragmentManager, "tocRegexDialog")
}
}
interface CallBack {
fun onTocRegexDialogResult(tocRegex: String) {}
}
}

@ -8,9 +8,12 @@ import android.text.TextUtils
import io.legado.app.App
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.*
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.dp
import io.legado.app.utils.getPrefString
import io.legado.app.utils.removePref
@ -20,10 +23,12 @@ import io.legado.app.utils.removePref
object ChapterProvider {
var viewWidth = 0
var viewHeight = 0
private var visibleWidth = 0
private var visibleHeight = 0
private var paddingLeft = 0
private var paddingTop = 0
var paddingLeft = 0
var paddingTop = 0
var visibleWidth = 0
var visibleHeight = 0
var visibleRight = 0
var visibleBottom = 0
private var lineSpacingExtra = 0f
private var paragraphSpacing = 0
var typeface: Typeface = Typeface.SANS_SERIF
@ -32,50 +37,12 @@ object ChapterProvider {
private var bodyIndent = BookHelp.bodyIndent
init {
upStyle(ReadBookConfig.durConfig)
}
fun upStyle(config: ReadBookConfig.Config) {
typeface = try {
val fontPath = App.INSTANCE.getPrefString(PreferKey.readBookFont)
if (!TextUtils.isEmpty(fontPath)) {
Typeface.createFromFile(fontPath)
} else {
Typeface.SANS_SERIF
}
} catch (e: Exception) {
App.INSTANCE.removePref(PreferKey.readBookFont)
Typeface.SANS_SERIF
}
//标题
titlePaint.isAntiAlias = true
titlePaint.color = config.textColor()
titlePaint.letterSpacing = config.letterSpacing
titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD)
//正文
contentPaint.isAntiAlias = true
contentPaint.color = config.textColor()
contentPaint.letterSpacing = config.letterSpacing
val bold = if (config.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold)
//间距
lineSpacingExtra = config.lineSpacingExtra.dp.toFloat()
paragraphSpacing = config.paragraphSpacing.dp
titlePaint.textSize = (config.textSize + 2).dp.toFloat()
contentPaint.textSize = config.textSize.dp.toFloat()
bodyIndent = BookHelp.bodyIndent
upSize(config)
}
fun upSize(config: ReadBookConfig.Config) {
paddingLeft = config.paddingLeft.dp
paddingTop = config.paddingTop.dp
visibleWidth = viewWidth - paddingLeft - config.paddingRight.dp
visibleHeight = viewHeight - paddingTop - config.paddingBottom.dp
upStyle()
}
/**
* 获取拆分完的章节数据
*/
fun getTextChapter(
bookChapter: BookChapter,
content: String,
@ -111,6 +78,8 @@ object ChapterProvider {
durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder)
}
}
textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString()
if (pageLines.size < textPages.size) {
pageLines.add(textPages.last().textLines.size)
}
@ -152,61 +121,39 @@ object ChapterProvider {
Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true
)
for (lineIndex in 0 until layout.lineCount) {
textPages.last().height = durY
durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
val textLine = TextLine(isTitle = true)
if (durY < visibleHeight) {
textPages.last().textLines.add(textLine)
} else {
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.last().text = stringBuilder.toString()
stringBuilder.clear()
pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length)
//新页面
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.add(TextPage())
textPages.last().textLines.add(textLine)
}
textLine.lineBottom = paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))
textLine.lineTop = paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))
textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + titlePaint.fontMetrics.descent
val words =
title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words)
textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex)
val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex != layout.lineCount - 1) {
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, titlePaint)
val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw)
val textChar = TextChar(
charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
)
textLine.textChars.add(textChar)
x = x1
}
addCharsToLineMiddle(textLine, words, titlePaint, desiredWidth, 0f)
} else {
//最后一行
textLine.text = "$words\n"
stringBuilder.append("\n")
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, titlePaint)
val x1 = x + cw
val textChar = TextChar(
charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
)
textLine.textChars.add(textChar)
x = x1
}
val x = if (ReadBookConfig.titleCenter)
(visibleWidth - layout.getLineWidth(lineIndex)) / 2
else 0f
addCharsToLineLast(textLine, words, stringBuilder, titlePaint, x)
}
}
durY += paragraphSpacing
@ -230,92 +177,192 @@ object ChapterProvider {
Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true
)
for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine(isTitle = false)
textPages.last().height = durY
durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
val textLine = TextLine()
if (durY < visibleHeight) {
textPages.last().textLines.add(textLine)
} else {
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.last().text = stringBuilder.toString()
stringBuilder.clear()
pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length)
//新页面
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.add(TextPage())
textPages.last().textLines.add(textLine)
}
textLine.lineBottom = paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))
textLine.lineTop = paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))
var words =
textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + contentPaint.fontMetrics.descent
val words =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words)
textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex)
val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex == 0 && layout.lineCount > 1) {
//第一行
addCharsToLineFirst(textLine, words, contentPaint, desiredWidth)
} else if (lineIndex == layout.lineCount - 1) {
//最后一行
addCharsToLineLast(textLine, words, stringBuilder, contentPaint, 0f)
} else {
//中间行
addCharsToLineMiddle(textLine, words, contentPaint, desiredWidth, 0f)
}
}
durY += paragraphSpacing
return durY
}
/**
* 有缩进,两端对齐
*/
private fun addCharsToLineFirst(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float
) {
var x = 0f
val icw = StaticLayout.getDesiredWidth(bodyIndent, contentPaint)
var x1 = x + icw
val textChar = TextChar(
charData = bodyIndent,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
for (i in 0..bodyIndent.lastIndex) {
val x1 = x + icw
textLine.addTextChar(
charData = bodyIndent[i].toString(),
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
words = words.replaceFirst(bodyIndent, "")
}
val words1 = words.replaceFirst(bodyIndent, "")
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x)
}
/**
* 无缩进,两端对齐
*/
private fun addCharsToLineMiddle(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float,
startX: Float
) {
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
var x = startX
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
x1 = if (i != words.lastIndex) x + cw + d else x + cw
val textChar1 = TextChar(
charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
val char = words[i]
val cw = StaticLayout.getDesiredWidth(char.toString(), textPaint)
val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw)
textLine.addTextChar(
charData = char.toString(),
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar1)
x = x1
}
} else if (lineIndex == layout.lineCount - 1) {
//最后一行
exceed(textLine, words)
}
/**
* 最后一行,自然排列
*/
private fun addCharsToLineLast(
textLine: TextLine,
words: String,
stringBuilder: StringBuilder,
textPaint: TextPaint,
startX: Float
) {
stringBuilder.append("\n")
textLine.text = "$words\n"
var x = 0f
var x = startX
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
val cw = StaticLayout.getDesiredWidth(char, textPaint)
val x1 = x + cw
val textChar = TextChar(
textLine.addTextChar(
charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
exceed(textLine, words)
}
/**
* 超出边界处理
*/
private fun exceed(textLine: TextLine, words: String) {
val endX = textLine.textChars.last().end
if (endX > visibleRight) {
val cc = (endX - visibleRight) / words.length
for (i in 0..words.lastIndex) {
textLine.getTextCharReverseAt(i).let {
val py = cc * (words.length - i)
it.start = it.start - py
it.end = it.end - py
}
}
}
}
/**
* 更新样式
*/
fun upStyle() {
typeface = try {
val fontPath = App.INSTANCE.getPrefString(PreferKey.readBookFont)
if (!TextUtils.isEmpty(fontPath)) {
Typeface.createFromFile(fontPath)
} else {
//中间行
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
val x1 = if (i != words.lastIndex) x + cw + d else x + cw
val textChar = TextChar(
charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom),
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop)
)
textLine.textChars.add(textChar)
x = x1
when (AppConfig.systemTypefaces) {
1 -> Typeface.SERIF
2 -> Typeface.MONOSPACE
else -> Typeface.SANS_SERIF
}
}
} catch (e: Exception) {
App.INSTANCE.removePref(PreferKey.readBookFont)
Typeface.SANS_SERIF
}
durY += paragraphSpacing
return durY
//标题
titlePaint.isAntiAlias = true
titlePaint.color = ReadBookConfig.durConfig.textColor()
titlePaint.letterSpacing = ReadBookConfig.letterSpacing
titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD)
//正文
contentPaint.isAntiAlias = true
contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold)
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra.dp.toFloat()
paragraphSpacing = ReadBookConfig.paragraphSpacing.dp
titlePaint.textSize = (ReadBookConfig.textSize + 2).dp.toFloat()
contentPaint.textSize = ReadBookConfig.textSize.dp.toFloat()
bodyIndent = BookHelp.bodyIndent
upSize()
}
/**
* 更新View尺寸
*/
fun upSize() {
paddingLeft = ReadBookConfig.paddingLeft.dp
paddingTop = ReadBookConfig.paddingTop.dp
visibleWidth = viewWidth - paddingLeft - ReadBookConfig.paddingRight.dp
visibleHeight = viewHeight - paddingTop - ReadBookConfig.paddingBottom.dp
visibleRight = paddingLeft + visibleWidth
visibleBottom = paddingTop + visibleHeight
}
}

@ -1,39 +0,0 @@
package io.legado.app.ui.book.read.page
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent
class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_replace -> {
val text = textView.text.substring(textView.selectionStart, textView.selectionEnd)
postEvent(EventBus.REPLACE, text)
mode?.finish()
return true
}
}
return false
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode?.menuInflater?.inflate(R.menu.content_select_action, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
}
}

@ -1,124 +1,591 @@
package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.read.page.entities.SelectPoint
import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.activity
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble)
var upView: ((TextPage) -> Unit)? = null
private val selectedPaint by lazy {
Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL
}
}
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble)
var selectStartLine = 0
var selectStartChar = 0
var selectEndLine = 0
var selectEndChar = 0
private var textPage: TextPage? = null
private var callBack: CallBack
private val visibleRect = RectF()
private val selectStart = arrayOf(0, 0, 0)
private val selectEnd = arrayOf(0, 0, 0)
private var textPage: TextPage = TextPage()
//滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f
private var pageOffset = 0f
fun setContent(textPage: TextPage?) {
init {
callBack = activity as CallBack
contentDescription = textPage.text
}
fun setContent(textPage: TextPage) {
this.textPage = textPage
contentDescription = textPage.text
invalidate()
}
fun upVisibleRect() {
visibleRect.set(
ChapterProvider.paddingLeft.toFloat(),
ChapterProvider.paddingTop.toFloat(),
ChapterProvider.visibleRight.toFloat(),
ChapterProvider.visibleBottom.toFloat()
)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
ReadBookConfig.durConfig.let {
ChapterProvider.viewWidth = w
ChapterProvider.viewHeight = h
ChapterProvider.upSize(ReadBookConfig.durConfig)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
return true
ChapterProvider.upSize()
upVisibleRect()
textPage.format()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
textPage?.let { textPage ->
canvas.clipRect(visibleRect)
drawPage(canvas)
}
/**
* 绘制页面
*/
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
textPage.textLines.forEach { textLine ->
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
textPaint.color = if (textLine.isReadAloud) {
context.accentColor
} else {
ReadBookConfig.durConfig.textColor()
if (!ReadBookConfig.isScroll) return
//滚动翻页
val nextPage = relativePage(1)
relativeOffset = relativeOffset(1)
nextPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
textLine.textChars.forEach {
canvas.drawText(
it.charData,
it.leftBottomPosition.x,
it.leftBottomPosition.y.toFloat(),
textPaint
relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) {
relativePage(2).textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
}
}
/**
* 绘制文字
*/
private fun drawChars(
canvas: Canvas,
textChars: List<TextChar>,
lineTop: Float,
lineBase: Float,
lineBottom: Float,
isTitle: Boolean,
isReadAloud: Boolean
) {
val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint
textPaint.color =
if (isReadAloud) context.accentColor else ReadBookConfig.durConfig.textColor()
textChars.forEach {
canvas.drawText(it.charData, it.start, lineBase, textPaint)
if (it.selected) {
canvas.drawRect(
it.leftBottomPosition.x,
it.rightTopPosition.y.toFloat(),
it.rightTopPosition.x,
it.leftBottomPosition.y.toFloat(),
selectedPaint
)
canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint)
}
}
}
/**
* 滚动事件
*/
fun onScroll(mOffset: Float) {
if (mOffset == 0f) return
var offset = mOffset
if (offset > maxScrollOffset) {
offset = maxScrollOffset
} else if (offset < -maxScrollOffset) {
offset = -maxScrollOffset
}
pageOffset += offset
if (pageOffset > 0) {
if (!pageFactory.hasPrev()) {
pageOffset = 0f
} else {
pageFactory.moveToPrev()
textPage = pageFactory.currentPage
pageOffset -= textPage.height
upView?.invoke(textPage)
}
} else if (pageOffset < -textPage.height) {
if (!pageFactory.hasNext()) {
pageOffset = -textPage.height.toFloat()
} else {
pageOffset += textPage.height
pageFactory.moveToNext()
textPage = pageFactory.currentPage
upView?.invoke(textPage)
}
}
invalidate()
}
fun resetPageOffset() {
pageOffset = 0f
}
fun selectText(x: Float, y: Float): SelectPoint? {
textPage?.let { textPage ->
/**
* 选择初始文字
*/
fun selectText(
x: Float,
y: Float,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
) {
if (!selectAble) return
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStartLine = lineIndex
selectStartChar = charIndex
selectEndLine = lineIndex
selectEndChar = charIndex
return SelectPoint(
textChar.leftBottomPosition.x,
textChar.leftBottomPosition.y.toFloat(),
textChar.rightTopPosition.x,
textChar.leftBottomPosition.y.toFloat()
)
selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 0
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(0, lineIndex, charIndex)
return
}
}
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
val nextPage = relativePage(1)
for ((lineIndex, textLine) in nextPage.textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(1, lineIndex, charIndex)
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(2, lineIndex, charIndex)
return
}
break
}
return
}
}
return null
}
/**
* 开始选择符移动
*/
fun selectStartMove(x: Float, y: Float) {
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 0 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(0, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 1 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(1, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 2 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(2, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
}
/**
* 结束选择符移动
*/
fun selectEndMove(x: Float, y: Float) {
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 0 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(0, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 0
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 1 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(1, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 2 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(2, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
}
/**
* 选择开始文字
*/
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectStart[0] = relativePage
selectStart[1] = lineIndex
selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
/**
* 选择结束文字
*/
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectEnd[0] = relativePage
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
private fun upSelectChars() {
val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
textChar.selected =
if (relativePos == selectStart[0]
&& relativePos == selectEnd[0]
&& lineIndex == selectStart[1]
&& lineIndex == selectEnd[1]
) {
charIndex in selectStart[2]..selectEnd[2]
} else if (relativePos == selectStart[0] && lineIndex == selectStart[1]) {
charIndex >= selectStart[2]
} else if (relativePos == selectEnd[0] && lineIndex == selectEnd[1]) {
charIndex <= selectEnd[2]
} else if (relativePos == selectStart[0] && relativePos == selectEnd[0]) {
lineIndex in (selectStart[1] + 1) until selectEnd[1]
} else if (relativePos == selectStart[0]) {
lineIndex > selectStart[1]
} else if (relativePos == selectEnd[0]) {
lineIndex < selectEnd[1]
} else {
relativePos in selectStart[0] + 1 until selectEnd[0]
}
}
}
}
invalidate()
}
private fun upSelectedStart(x: Float, y: Float) {
callBack.upSelectedStart(x, y + callBack.headerHeight)
}
private fun upSelectedEnd(x: Float, y: Float) {
callBack.upSelectedEnd(x, y + callBack.headerHeight)
}
fun cancelSelect() {
val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
relativePage(relativePos).textLines.forEach { textLine ->
textLine.textChars.forEach {
it.selected = false
}
}
}
invalidate()
callBack.onCancelSelect()
}
val selectedText: String
get() {
val stringBuilder = StringBuilder()
for (relativePos in selectStart[0]..selectEnd[0]) {
val textPage = relativePage(relativePos)
if (relativePos == selectStart[0] && relativePos == selectEnd[0]) {
for (lineIndex in selectStart[1]..selectEnd[1]) {
if (lineIndex == selectStart[1] && lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2],
selectEnd[2] + 1
)
)
} else if (lineIndex == selectStart[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2]
)
)
} else if (lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos == selectStart[0]) {
for (lineIndex in selectStart[1] until relativePage(relativePos).textLines.size) {
if (lineIndex == selectStart[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2]
)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos == selectEnd[0]) {
for (lineIndex in 0..selectEnd[1]) {
if (lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos in selectStart[0] + 1 until selectEnd[0]) {
for (lineIndex in selectStart[1]..selectEnd[1]) {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
}
return stringBuilder.toString()
}
private fun selectToInt(page: Int, line: Int, char: Int): Int {
return page * 1000000 + line * 100000 + char
}
private fun selectToInt(select: Array<Int>): Int {
return select[0] * 1000000 + select[1] * 100000 + select[2]
}
private fun relativeOffset(relativePos: Int): Float {
return when (relativePos) {
0 -> pageOffset
1 -> pageOffset + textPage.height
else -> pageOffset + textPage.height + pageFactory.nextPage.height
}
}
private fun relativePage(relativePos: Int): TextPage {
return when (relativePos) {
0 -> textPage
1 -> pageFactory.nextPage
else -> pageFactory.nextPagePlus
}
}
interface CallBack {
fun upSelectedStart(x: Float, y: Float)
fun upSelectedEnd(x: Float, y: Float)
fun onCancelSelect()
val headerHeight: Int
val pageFactory: TextPageFactory
}
}

@ -3,78 +3,65 @@ package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.SelectPoint
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.view_book_page.view.*
import java.util.*
class ContentView : FrameLayout {
var callBack: CallBack? = null
private var headerHeight = 0
private var pageSize: Int = 0
class ContentView(context: Context) : FrameLayout(context) {
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
fun init() {
init {
//设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background))
inflate(context, R.layout.view_book_page, this)
upStyle()
upTime()
content_text_view.upView = {
tv_bottom_left.text = it.title
setPageIndex(it.index, it.pageSize)
}
}
fun upStyle() {
ReadBookConfig.durConfig.apply {
ReadBookConfig.apply {
tv_top_left.typeface = ChapterProvider.typeface
tv_top_right.typeface = ChapterProvider.typeface
tv_bottom_left.typeface = ChapterProvider.typeface
tv_bottom_right.typeface = ChapterProvider.typeface
if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) {
//显示状态栏时隐藏header
ll_header.visible()
ll_header.layoutParams =
ll_header.layoutParams.apply { height = context.getStatusBarHeight() }
if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams.apply {
height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding(
headerPaddingLeft.dp,
headerPaddingTop.dp,
headerPaddingRight.dp,
headerPaddingBottom.dp
)
headerHeight = ll_header.height
ll_header.visible()
page_panel.setPadding(0, 0, 0, 0)
} else {
ll_header.gone()
headerHeight = context.getStatusBarHeight()
page_panel.setPadding(0, headerHeight, 0, 0)
}
content_text_view.setPadding(
paddingLeft.dp,
paddingTop.dp,
paddingRight.dp,
paddingBottom.dp
)
page_panel.setPadding(0, context.statusBarHeight, 0, 0)
}
ll_footer.setPadding(
footerPaddingLeft.dp,
footerPaddingTop.dp,
footerPaddingRight.dp,
footerPaddingBottom.dp
)
textColor().let {
content_text_view.upVisibleRect()
durConfig.textColor().let {
tv_top_left.setTextColor(it)
tv_top_right.setTextColor(it)
tv_bottom_left.setTextColor(it)
@ -83,6 +70,15 @@ class ContentView : FrameLayout {
}
}
val headerHeight: Int
get() {
return if (ReadBookConfig.hideStatusBar) {
ll_header.height
} else {
context.statusBarHeight
}
}
fun setBg(bg: Drawable?) {
page_panel.background = bg
}
@ -95,60 +91,64 @@ class ContentView : FrameLayout {
tv_top_right.text = context.getString(R.string.battery_show, battery)
}
fun setContent(textPage: TextPage?) {
fun setContent(textPage: TextPage) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage)
if (textPage != null) {
tv_bottom_left.text = textPage.title
pageSize = textPage.pageSize
setPageIndex(textPage.index)
}
fun resetPageOffset() {
content_text_view.resetPageOffset()
}
@SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?) {
fun setPageIndex(pageIndex: Int?, pageSize: Int) {
pageIndex?.let {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}"
}
}
fun onScroll(offset: Float) {
content_text_view.onScroll(offset)
}
fun upSelectAble(selectAble: Boolean) {
content_text_view.selectAble = selectAble
}
fun selectText(e: MotionEvent): SelectPoint? {
fun selectText(
e: MotionEvent,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
) {
val y = e.y - headerHeight
val selectPoint = content_text_view.selectText(e.x, y)
selectPoint?.let {
it.startY = it.startY + headerHeight
it.endY = it.endY + headerHeight
}
return selectPoint
return content_text_view.selectText(e.x, y, select)
}
fun selectStartMove(x: Float, y: Float) {
content_text_view.selectStartMove(x, y)
content_text_view.selectStartMove(x, y - headerHeight)
}
fun selectEndMove(x: Float, y: Float) {
content_text_view.selectEndMove(x, y)
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
fun scrollTo(pos: Int?) {
if (pos != null) {
content_text_view.post {
}
}
fun selectEndMove(x: Float, y: Float) {
content_text_view.selectEndMove(x, y - headerHeight)
}
fun scrollToBottom() {
content_text_view.post {
}
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
interface CallBack {
fun scrollToLine(line: Int)
fun scrollToLast()
fun cancelSelect() {
content_text_view.cancelSelect()
}
val selectedText: String get() = content_text_view.selectedText
}

@ -1,24 +1,19 @@
package io.legado.app.ui.book.read.page
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChapter
interface DataSource {
val isScrollDelegate: Boolean
val pageIndex: Int
val pageIndex: Int get() = ReadBook.durChapterPos()
fun setPageIndex(pageIndex: Int)
val currentChapter: TextChapter?
fun getChapterPosition(): Int
val nextChapter: TextChapter?
fun getCurrentChapter(): TextChapter?
fun getNextChapter(): TextChapter?
fun getPreviousChapter(): TextChapter?
val prevChapter: TextChapter?
fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean
}

@ -8,13 +8,15 @@ abstract class PageFactory<DATA>(protected val dataSource: DataSource) {
abstract fun moveToNext():Boolean
abstract fun moveToPrevious(): Boolean
abstract fun moveToPrev(): Boolean
abstract fun nextPage(): DATA?
abstract val nextPage: DATA
abstract fun previousPage(): DATA?
abstract val prevPage: DATA
abstract fun currentPage(): DATA?
abstract val currentPage: DATA
abstract val nextPagePlus: DATA
abstract fun hasNext(): Boolean

@ -6,30 +6,26 @@ import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.ui.book.read.page.entities.SelectPoint
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt
class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs),
ContentView.CallBack,
DataSource {
var callBack: CallBack? = null
var pageFactory: TextPageFactory? = null
private var pageDelegate: PageDelegate? = null
var callBack: CallBack
var pageFactory: TextPageFactory
var pageDelegate: PageDelegate? = null
var prevPage: ContentView
var curPage: ContentView
var nextPage: ContentView
init {
callBack = activity as? CallBack
callBack = activity as CallBack
prevPage = ContentView(context)
addView(prevPage)
nextPage = ContentView(context)
@ -39,8 +35,7 @@ class PageView(context: Context, attrs: AttributeSet) :
upBg()
setWillNotDraw(false)
pageFactory = TextPageFactory(this)
upPageAnim(context.getPrefInt(PreferKey.pageAnim))
curPage.callBack = this
upPageAnim()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@ -67,33 +62,34 @@ class PageView(context: Context, attrs: AttributeSet) :
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
callBack?.screenOffTimerStart()
return pageDelegate?.onTouch(event) ?: super.onTouchEvent(event)
pageDelegate?.onTouch(event)
callBack.screenOffTimerStart()
return true
}
fun onDestroy() {
pageDelegate?.onDestroy()
curPage.cancelSelect()
}
fun fillPage(direction: PageDelegate.Direction) {
when (direction) {
PageDelegate.Direction.PREV -> {
pageFactory?.moveToPrevious()
pageFactory.moveToPrev()
upContent()
if (isScrollDelegate) {
curPage.scrollToBottom()
}
}
PageDelegate.Direction.NEXT -> {
pageFactory?.moveToNext()
pageFactory.moveToNext()
upContent()
if (isScrollDelegate) {
curPage.scrollTo(0)
}
}
else -> Unit
}
}
fun upPageAnim(pageAnim: Int) {
fun upPageAnim() {
pageDelegate?.onDestroy()
pageDelegate = null
pageDelegate = when (pageAnim) {
pageDelegate = when (ReadBookConfig.pageAnim) {
0 -> CoverPageDelegate(this)
1 -> SlidePageDelegate(this)
2 -> SimulationPageDelegate(this)
@ -103,36 +99,26 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent()
}
fun upContent(position: Int = 0) {
pageFactory?.let {
when (position) {
-1 -> prevPage.setContent(it.previousPage())
1 -> nextPage.setContent(it.nextPage())
fun upContent(relativePosition: Int = 0) {
if (ReadBookConfig.isScroll) {
curPage.setContent(pageFactory.currentPage)
} else {
when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage)
1 -> nextPage.setContent(pageFactory.nextPage)
else -> {
curPage.setContent(it.currentPage())
nextPage.setContent(it.nextPage())
prevPage.setContent(it.previousPage())
if (isScrollDelegate) {
curPage.scrollTo(ReadBook.textChapter()?.getStartLine(ReadBook.durChapterPos()))
}
}
curPage.setContent(pageFactory.currentPage)
nextPage.setContent(pageFactory.nextPage)
prevPage.setContent(pageFactory.prevPage)
}
if (isScrollDelegate) {
prevPage.scrollToBottom()
}
}
callBack?.screenOffTimerStart()
callBack.screenOffTimerStart()
}
fun moveToPrevPage(noAnim: Boolean = true) {
if (noAnim) {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
curPage.scrollTo(it.getStartLine(pageIndex - 1))
}
} else {
fillPage(PageDelegate.Direction.PREV)
}
} else {
pageDelegate?.start(PageDelegate.Direction.PREV)
}
@ -140,24 +126,14 @@ class PageView(context: Context, attrs: AttributeSet) :
fun moveToNextPage(noAnim: Boolean = true) {
if (noAnim) {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
curPage.scrollTo(it.getStartLine(pageIndex + 1))
}
} else {
fillPage(PageDelegate.Direction.NEXT)
}
} else {
pageDelegate?.start(PageDelegate.Direction.NEXT)
}
}
fun upSelectAble(selectAble: Boolean) {
pageDelegate?.upSelectAble(selectAble)
curPage.upSelectAble(selectAble)
}
fun upStyle() {
ChapterProvider.upStyle()
curPage.upStyle()
prevPage.upStyle()
nextPage.upStyle()
@ -184,38 +160,19 @@ class PageView(context: Context, attrs: AttributeSet) :
nextPage.upBattery(battery)
}
fun selectStartMove(x: Float, y: Float) {
curPage.selectStartMove(x, y)
}
fun selectEndMove(x: Float, y: Float) {
curPage.selectEndMove(x, y)
}
override val isScrollDelegate: Boolean
get() = pageDelegate is ScrollPageDelegate
override val pageIndex: Int
get() = ReadBook.durChapterPos()
override fun setPageIndex(pageIndex: Int) {
callBack?.setPageIndex(pageIndex)
}
override fun getChapterPosition(): Int {
return ReadBook.durChapterIndex
}
override fun getCurrentChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null
override val currentChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
}
override fun getNextChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null
override val nextChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(1) else null
}
override fun getPreviousChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null
override val prevChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null
}
override fun hasNextChapter(): Boolean {
@ -223,47 +180,12 @@ class PageView(context: Context, attrs: AttributeSet) :
}
override fun hasPrevChapter(): Boolean {
callBack?.let {
return ReadBook.durChapterIndex > 0
}
return false
}
override fun scrollToLine(line: Int) {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
val pageIndex = it.getPageIndex(line)
curPage.setPageIndex(pageIndex)
callBack?.setPageIndex(pageIndex)
}
}
}
override fun scrollToLast() {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
callBack?.setPageIndex(it.lastIndex())
curPage.setPageIndex(it.lastIndex())
}
}
}
interface CallBack {
val isInitFinish: Boolean
/**
* 保存页数
*/
fun setPageIndex(pageIndex: Int)
/**
* 点击屏幕中间
*/
fun clickCenter()
fun screenOffTimerStart()
fun selectText(selectPoint: SelectPoint)
}
}

@ -6,91 +6,107 @@ import io.legado.app.ui.book.read.page.entities.TextPage
class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource) {
override fun hasPrev(): Boolean = with(dataSource) {
return if (isScrollDelegate) {
hasPrevChapter()
} else {
hasPrevChapter() || pageIndex > 0
}
return hasPrevChapter() || pageIndex > 0
}
override fun hasNext(): Boolean = with(dataSource) {
return if (isScrollDelegate) {
hasNextChapter()
} else {
hasNextChapter()
|| getCurrentChapter()?.isLastIndex(pageIndex) != true
}
return hasNextChapter() || currentChapter?.isLastIndex(pageIndex) != true
}
override fun moveToFirst() {
dataSource.setPageIndex(0)
ReadBook.setPageIndex(0)
}
override fun moveToLast() = with(dataSource) {
getCurrentChapter()?.let {
currentChapter?.let {
if (it.pageSize() == 0) {
setPageIndex(0)
ReadBook.setPageIndex(0)
} else {
setPageIndex(it.pageSize().minus(1))
ReadBook.setPageIndex(it.pageSize().minus(1))
}
} ?: setPageIndex(0)
} ?: ReadBook.setPageIndex(0)
}
override fun moveToNext(): Boolean = with(dataSource) {
return if (hasNext()) {
if (getCurrentChapter()?.isLastIndex(pageIndex) == true
|| isScrollDelegate
) {
if (currentChapter?.isLastIndex(pageIndex) == true) {
ReadBook.moveToNextChapter(false)
} else {
setPageIndex(pageIndex.plus(1))
ReadBook.setPageIndex(pageIndex.plus(1))
}
true
} else
false
}
override fun moveToPrevious(): Boolean = with(dataSource) {
override fun moveToPrev(): Boolean = with(dataSource) {
return if (hasPrev()) {
if (pageIndex <= 0 || isScrollDelegate) {
if (pageIndex <= 0) {
ReadBook.moveToPrevChapter(false)
} else {
setPageIndex(pageIndex.minus(1))
ReadBook.setPageIndex(pageIndex.minus(1))
}
true
} else
false
}
override fun currentPage(): TextPage? = with(dataSource) {
return if (isScrollDelegate) {
getCurrentChapter()?.scrollPage()
} else {
getCurrentChapter()?.page(pageIndex)
override val currentPage: TextPage
get() = with(dataSource) {
currentChapter?.let {
return@with it.page(pageIndex)
?: TextPage(title = it.title).format()
}
return TextPage().format()
}
override fun nextPage(): TextPage? = with(dataSource) {
if (isScrollDelegate) {
return getNextChapter()?.scrollPage()
}
getCurrentChapter()?.let {
override val nextPage: TextPage
get() = with(dataSource) {
currentChapter?.let {
if (pageIndex < it.pageSize() - 1) {
return getCurrentChapter()?.page(pageIndex + 1)?.removePageAloudSpan()
return@with it.page(pageIndex + 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
}
return getNextChapter()?.page(0)?.removePageAloudSpan()
nextChapter?.let {
return@with it.page(0)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
override fun previousPage(): TextPage? = with(dataSource) {
if (isScrollDelegate) {
return getPreviousChapter()?.scrollPage()
return TextPage().format()
}
override val prevPage: TextPage
get() = with(dataSource) {
if (pageIndex > 0) {
return getCurrentChapter()?.page(pageIndex - 1)?.removePageAloudSpan()
currentChapter?.let {
return@with it.page(pageIndex - 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
return getPreviousChapter()?.lastPage()?.removePageAloudSpan()
}
prevChapter?.let {
return@with it.lastPage()?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
return TextPage().format()
}
override val nextPagePlus: TextPage
get() = with(dataSource) {
currentChapter?.let {
if (pageIndex < it.pageSize() - 2) {
return@with it.page(pageIndex + 2)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
nextChapter?.let { nc ->
if (pageIndex < it.pageSize() - 1) {
return@with nc.page(0)?.removePageAloudSpan()
?: TextPage(title = nc.title).format()
}
return@with nc.page(1)?.removePageAloudSpan()
?: TextPage(title = nc.title).format()
}
}
return TextPage().format()
}
}

@ -6,19 +6,30 @@ import kotlin.math.abs
abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) {
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
override fun onTouch(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
if (isTextSelected) {
selectText(event)
} else {
onScroll(event)
}
}
}
super.onTouch(event)
}
private fun onScroll(event: MotionEvent) {
//判断是否移动了
if (!isMoved) {
if (abs(distanceX) > abs(distanceY)) {
if (distanceX < 0) {
isMoved = abs(startX - event.x) > slop
|| abs(startX - event.x) > abs(startY - event.y)
if (isMoved) {
if (event.x - startX > 0) {
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
return
}
setDirection(Direction.PREV)
setBitmap()
@ -26,21 +37,19 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
//如果不存在表示没有下一页了
if (!hasNext()) {
noNext = true
return true
return
}
setDirection(Direction.NEXT)
setBitmap()
}
isMoved = true
}
}
if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) distanceX < 0 else distanceX > 0
isCancel = if (mDirection == Direction.NEXT) touchX > lastX else touchX < lastX
isRunning = true
//设置触摸点
setTouchPoint(e2.x, e2.y)
setTouchPoint(event.x, event.y)
}
return isMoved
}
}

@ -6,15 +6,14 @@ import android.graphics.Canvas
import android.graphics.RectF
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import com.google.android.material.snackbar.Snackbar
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.screenshot
import kotlin.math.abs
@ -25,42 +24,36 @@ abstract class PageDelegate(protected val pageView: PageView) :
pageView.width * 0.66f, pageView.height * 0.66f
)
protected val context: Context = pageView.context
protected val slop = ViewConfiguration.get(context).scaledTouchSlop
//起始点
protected var startX: Float = 0.toFloat()
protected var startY: Float = 0.toFloat()
protected var startX: Float = 0f
protected var startY: Float = 0f
//上一个触碰点
protected var lastX: Float = 0f
protected var lastY: Float = 0f
//触碰点
protected var touchX: Float = 0.toFloat()
protected var touchY: Float = 0.toFloat()
protected var touchX: Float = 0f
protected var touchY: Float = 0f
protected val nextPage: ContentView
get() = pageView.nextPage
protected val curPage: ContentView
get() = pageView.curPage
protected val prevPage: ContentView
get() = pageView.prevPage
protected val nextPage: ContentView get() = pageView.nextPage
protected val curPage: ContentView get() = pageView.curPage
protected val prevPage: ContentView get() = pageView.prevPage
protected var bitmap: Bitmap? = null
protected var viewWidth: Int = pageView.width
protected var viewHeight: Int = pageView.height
//textView在顶端或低端
protected var atTop: Boolean = false
protected var atBottom: Boolean = false
private val snackBar: Snackbar by lazy {
Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT)
}
private val scroller: Scroller by lazy {
Scroller(pageView.context, FastOutLinearInInterpolator())
Scroller(pageView.context, DecelerateInterpolator())
}
private val detector: GestureDetector by lazy {
GestureDetector(pageView.context, this).apply {
setIsLongpressEnabled(context.getPrefBoolean(PreferKey.textSelectAble))
}
GestureDetector(pageView.context, this)
}
var isMoved = false
@ -71,32 +64,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
var isCancel = false
var isRunning = false
var isStarted = false
var isTextSelected = false
var selectedOnDown = false
var firstRelativePage = 0
var firstLineIndex: Int = 0
var firstCharIndex: Int = 0
init {
curPage.resetPageOffset()
}
open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x
startY = y
lastX = x
lastY = y
touchX = x
touchY = y
if (invalidate) {
invalidate()
pageView.invalidate()
}
}
open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
lastX = touchX
lastY = touchY
touchX = x
touchY = y
if (invalidate) {
invalidate()
pageView.invalidate()
}
onScroll()
}
fun upSelectAble(selectAble: Boolean) {
detector.setIsLongpressEnabled(selectAble)
}
protected fun invalidate() {
open fun fling(
startX: Int, startY: Int, velocityX: Int, velocityY: Int,
minX: Int, maxX: Int, minY: Int, maxY: Int
) {
scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)
isRunning = true
isStarted = true
pageView.invalidate()
}
@ -110,28 +121,21 @@ abstract class PageDelegate(protected val pageView: PageView) :
)
isRunning = true
isStarted = true
invalidate()
pageView.invalidate()
}
protected fun stopScroll() {
private fun stopScroll() {
isRunning = false
isStarted = false
invalidate()
if (pageView.isScrollDelegate) {
pageView.postDelayed({
bitmap?.recycle()
bitmap = null
}, 100)
} else {
pageView.invalidate()
bitmap?.recycle()
bitmap = null
}
}
fun setViewSize(width: Int, height: Int) {
open fun setViewSize(width: Int, height: Int) {
viewWidth = width
viewHeight = height
invalidate()
pageView.invalidate()
centerRectF.set(
width * 0.33f, height * 0.33f,
width * 0.66f, height * 0.66f
@ -142,7 +146,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
setTouchPoint(scroller.finalX.toFloat(), scroller.finalY.toFloat(), false)
onAnimStop()
stopScroll()
}
@ -182,14 +185,13 @@ abstract class PageDelegate(protected val pageView: PageView) :
onAnimStart()
}
abstract fun onAnimStart()//scroller start
open fun onAnimStart() {}//scroller start
abstract fun onDraw(canvas: Canvas)//绘制
open fun onDraw(canvas: Canvas) {}//绘制
abstract fun onAnimStop()//scroller finish
open fun onAnimStop() {}//scroller finish
open fun onScroll() {//移动contentView, slidePage
}
open fun onScroll() {}//移动contentView, slidePage
@CallSuper
open fun setDirection(direction: Direction) {
@ -208,17 +210,28 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 触摸事件处理
*/
@CallSuper
open fun onTouch(event: MotionEvent): Boolean {
if (isStarted) return false
curPage.dispatchTouchEvent(event)
return detector.onTouchEvent(event)
open fun onTouch(event: MotionEvent) {
if (isStarted) return
if (!detector.onTouchEvent(event)) {
//GestureDetector.onFling小幅移动不会触发,所以要自己判断
if (event.action == MotionEvent.ACTION_UP && isMoved) {
if (selectedOnDown) {
selectedOnDown = false
}
if (!noNext) onAnimStart()
}
}
}
/**
* 按下
*/
override fun onDown(e: MotionEvent): Boolean {
// abort()
if (isTextSelected) {
curPage.cancelSelect()
isTextSelected = false
selectedOnDown = true
}
//是否移动
isMoved = false
//是否存在下一章
@ -238,25 +251,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 单击
*/
override fun onSingleTapUp(e: MotionEvent): Boolean {
if (selectedOnDown) {
selectedOnDown = false
return true
}
val x = e.x
val y = e.y
if (centerRectF.contains(x, y)) {
pageView.callBack?.clickCenter()
pageView.callBack.clickCenter()
setTouchPoint(x, y)
} else {
if (x > viewWidth / 2 ||
AppConfig.clickAllNext
) {
//设置动画方向
if (!hasNext()) {
return true
}
if (!hasNext()) return true
setDirection(Direction.NEXT)
setBitmap()
} else {
if (!hasPrev()) {
return true
}
if (!hasPrev()) return true
setDirection(Direction.PREV)
setBitmap()
}
@ -270,30 +283,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 长按选择
*/
override fun onLongPress(e: MotionEvent) {
val textChar = curPage.selectText(e)
textChar?.let {
pageView.callBack?.selectText(it)
curPage.selectText(e) { relativePage, lineIndex, charIndex ->
isTextSelected = true
firstRelativePage = relativePage
firstLineIndex = lineIndex
firstCharIndex = charIndex
}
}
/**
* 移动结束
*/
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
if (!noNext) onAnimStart()
return true
protected fun selectText(event: MotionEvent) {
curPage.selectText(event) { relativePage, lineIndex, charIndex ->
when {
relativePage > firstRelativePage -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
relativePage < firstRelativePage -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
lineIndex > firstLineIndex -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
lineIndex < firstLineIndex -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
charIndex > firstCharIndex -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
else -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
}
}
}
/**
* 判断是否有上一页
*/
fun hasPrev(): Boolean {
val hasPrev = pageView.pageFactory?.hasPrev() == true
val hasPrev = pageView.pageFactory.hasPrev()
if (!hasPrev) {
if (!snackBar.isShown) {
snackBar.setText("没有上一页")
@ -307,7 +340,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 判断是否有下一页
*/
fun hasNext(): Boolean {
val hasNext = pageView.pageFactory?.hasNext() == true
val hasNext = pageView.pageFactory.hasNext()
if (!hasNext) {
if (!snackBar.isShown) {
snackBar.setText("没有下一页")
@ -317,24 +350,12 @@ abstract class PageDelegate(protected val pageView: PageView) :
return hasNext
}
open fun onDestroy() {
bitmap?.recycle()
}
enum class Direction {
NONE, PREV, NEXT
}
fun MotionEvent.toAction(action: Int): MotionEvent {
return MotionEvent.obtain(
downTime,
eventTime,
action,
x,
y,
pressure,
size,
metaState,
xPrecision,
yPrecision,
deviceId,
edgeFlags
)
}
}

@ -1,119 +1,61 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.VelocityTracker
import io.legado.app.ui.book.read.page.PageView
import kotlin.math.abs
class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
private val bitmapMatrix = Matrix()
// 滑动追踪的时间
private val velocityDuration = 1000
//速度追踪器
private val mVelocity: VelocityTracker = VelocityTracker.obtain()
override fun onAnimStart() {
if (!atTop && !atBottom) {
stopScroll()
return
//惯性滚动
fling(
0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(),
0, 0, -10 * viewHeight, 10 * viewHeight
)
}
val distanceY: Float
when (mDirection) {
Direction.NEXT -> distanceY =
if (isCancel) {
var dis = viewHeight - startY + touchY
if (dis > viewHeight) {
dis = viewHeight.toFloat()
}
viewHeight - dis
} else {
-(touchY + (viewHeight - startY))
}
else -> distanceY =
if (isCancel) {
-(touchY - startY)
} else {
viewHeight - (touchY - startY)
}
}
startScroll(0, touchY.toInt(), 0, distanceY.toInt())
}
override fun onDraw(canvas: Canvas) {
if (atTop || atBottom) {
val offsetY = touchY - startY
if ((mDirection == Direction.NEXT && offsetY > 0)
|| (mDirection == Direction.PREV && offsetY < 0)
) return
val distanceY = if (offsetY > 0) offsetY - viewHeight else offsetY + viewHeight
if (atTop && mDirection == Direction.PREV) {
bitmap?.let {
bitmapMatrix.setTranslate(0.toFloat(), distanceY)
canvas.drawBitmap(it, bitmapMatrix, null)
}
} else if (atBottom && mDirection == Direction.NEXT) {
bitmap?.let {
bitmapMatrix.setTranslate(0.toFloat(), distanceY)
canvas.drawBitmap(it, bitmapMatrix, null)
}
}
}
}
override fun onAnimStop() {
if (!isCancel) {
pageView.fillPage(mDirection)
}
override fun onScroll() {
curPage.onScroll(touchY - lastY)
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isMoved && abs(distanceX) < abs(distanceY)) {
if (distanceY < 0) {
if (atTop) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage.dispatchTouchEvent(event)
event.recycle()
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
}
setDirection(Direction.PREV)
setBitmap()
override fun onTouch(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
setStartPoint(event.x, event.y)
abort()
mVelocity.clear()
}
MotionEvent.ACTION_MOVE -> {
if (isTextSelected) {
selectText(event)
} else {
if (atBottom) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage.dispatchTouchEvent(event)
event.recycle()
//如果不存在表示没有下一页了
if (!hasNext()) {
noNext = true
return true
onScroll(event)
}
setDirection(Direction.NEXT)
setBitmap()
}
}
isMoved = true
super.onTouch(event)
}
if ((atTop && mDirection != Direction.PREV) || (atBottom && mDirection != Direction.NEXT) || mDirection == Direction.NONE) {
//传递触摸事件到textView
curPage.dispatchTouchEvent(e2)
private fun onScroll(event: MotionEvent) {
mVelocity.addMovement(event)
mVelocity.computeCurrentVelocity(velocityDuration)
setTouchPoint(event.x, event.y)
if (!isMoved) {
isMoved = abs(startX - event.x) > slop || abs(startY - event.y) > slop
}
if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) distanceY < 0 else distanceY > 0
isRunning = true
//设置触摸点
setTouchPoint(e2.x, e2.y)
}
return isMoved
}
override fun onDestroy() {
super.onDestroy()
mVelocity.recycle()
}
}

@ -40,7 +40,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private var mMiddleY = 0f
private var mDegrees = 0f
private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter = ColorMatrixColorFilter(
private var mColorMatrixFilter = ColorMatrixColorFilter(
ColorMatrix(
floatArrayOf(
1f, 0f, 0f, 0f, 0f,
@ -55,7 +55,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
// 是否属于右上左下
private var mIsRtOrLb = false
private var mMaxLength = 0f
private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
// 背面颜色组
private var mBackShadowColors: IntArray
// 前面颜色组
@ -71,15 +71,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private var mFrontShadowDrawableVLR: GradientDrawable
private var mFrontShadowDrawableVRL: GradientDrawable
private val mPaint: Paint = Paint()
private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL }
private var curBitmap: Bitmap? = null
private var prevBitmap: Bitmap? = null
private var nextBitmap: Bitmap? = null
init {
mMaxLength = hypot(viewWidth.toDouble(), viewWidth.toDouble()).toFloat()
mPaint.style = Paint.Style.FILL
//设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color)
@ -115,13 +113,18 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT
}
override fun setViewSize(width: Int, height: Int) {
super.setViewSize(width, height)
mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
}
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
super.setStartPoint(x, y, invalidate)
calcCornerXY(x, y)
}
override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
super.setTouchPoint(x, y, invalidate)
super.setTouchPoint(x, y, false)
//触摸y中间位置吧y变成屏幕高度
if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66)
|| mDirection == Direction.PREV
@ -134,6 +137,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
) {
touchY = 1f
}
pageView.invalidate()
}
override fun setDirection(direction: Direction) {
@ -171,7 +175,6 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
override fun onAnimStart() {
var dx: Float
val dy: Float
// dx 水平方向滑动的距离,负值会使滚动向左滚动
// dy 垂直方向滑动的距离,负值会使滚动向上滚动
if (isCancel) {
dx = if (mCornerX > 0 && mDirection == Direction.NEXT) {
@ -215,19 +218,23 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
}
override fun onDraw(canvas: Canvas) {
if (mDirection === Direction.NEXT) {
when (mDirection) {
Direction.NEXT -> {
calcPoints()
drawCurrentPageArea(canvas, curBitmap, mPath0)
drawCurrentPageArea(canvas, curBitmap)
drawNextPageAreaAndShadow(canvas, nextBitmap)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap)
} else {
}
Direction.PREV -> {
calcPoints()
drawCurrentPageArea(canvas, prevBitmap, mPath0)
drawCurrentPageArea(canvas, prevBitmap)
drawNextPageAreaAndShadow(canvas, curBitmap)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap)
}
else -> return
}
}
/**
@ -254,7 +261,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
val left: Int
val right: Int
if (mIsRtOrLb) {
left = mBezierStart1.x.toInt() - 1
left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR
} else {
@ -333,15 +340,14 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
var mCurrentPageShadow: GradientDrawable
if (mIsRtOrLb) {
leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25
rightX = (mBezierControl1.x + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableVLR
} else {
leftX = mBezierControl1.x.toInt() - 25
rightX = mBezierControl1.x.toInt() + 1
leftX = (mBezierControl1.x - 25).toInt()
rightX = (mBezierControl1.x + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableVRL
}
var rotateDegrees: Float =
Math.toDegrees(
var rotateDegrees = Math.toDegrees(
atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
@ -368,24 +374,25 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
if (mIsRtOrLb) {
leftX = mBezierControl2.y.toInt()
rightX = mBezierControl2.y.toInt() + 25
rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB
} else {
leftX = mBezierControl2.y.toInt() - 25
rightX = mBezierControl2.y.toInt() + 1
leftX = (mBezierControl2.y - 25).toInt()
rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT
}
rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float =
if (mBezierControl2.y < 0) mBezierControl2.y - viewHeight else mBezierControl2.y
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt()
val temp =
if (mBezierControl2.y < 0) (mBezierControl2.y - viewHeight).toDouble()
else mBezierControl2.y.toDouble()
val hmg = hypot(mBezierControl2.x.toDouble(), temp)
if (hmg > mMaxLength)
mCurrentPageShadow.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg, rightX
(mBezierControl2.x - 25 - hmg).toInt(), leftX,
(mBezierControl2.x + mMaxLength - hmg).toInt(), rightX
)
else
mCurrentPageShadow.setBounds(
@ -396,6 +403,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore()
}
//
private fun drawNextPageAreaAndShadow(
canvas: Canvas,
bitmap: Bitmap?
@ -443,10 +451,10 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore()
}
//
private fun drawCurrentPageArea(
canvas: Canvas,
bitmap: Bitmap?,
path: Path
bitmap: Bitmap?
) {
bitmap ?: return
mPath0.reset()
@ -457,11 +465,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close()
canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path)
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(path, Region.Op.XOR)
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.restore()
@ -480,8 +489,10 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private fun calcPoints() {
mTouchX = touchX
mTouchY = touchY
mMiddleX = (mTouchX + mCornerX) / 2
mMiddleY = (mTouchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
@ -492,6 +503,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} else {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
mBezierStart1.y = mCornerY.toFloat()
//固定左边上下两个点
@ -500,24 +512,27 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
if (mBezierStart1.x < 0)
mBezierStart1.x = viewWidth - mBezierStart1.x
val f1: Float = abs(mCornerX - mTouchX)
val f2: Float = viewWidth * f1 / mBezierStart1.x
val f1 = abs(mCornerX - mTouchX)
val f2 = viewWidth * f1 / mBezierStart1.x
mTouchX = abs(mCornerX - f2)
val f3: Float = abs(mCornerX - mTouchX) * abs(mCornerY - mTouchY) / f1
val f3 = abs(mCornerX - mTouchX) * abs(mCornerY - mTouchY) / f1
mTouchY = abs(mCornerY - f3)
mMiddleX = (mTouchX + mCornerX) / 2
mMiddleY = (mTouchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
mBezierControl2.y = if ((mCornerY - mMiddleY).toInt() == 0) {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = (mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2)
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
}
}
mBezierStart2.x = mCornerX.toFloat()

@ -1,8 +0,0 @@
package io.legado.app.ui.book.read.page.entities
data class SelectPoint(
var startX: Float,
var startY: Float,
var endX: Float,
var endY: Float
)

@ -2,7 +2,7 @@ package io.legado.app.ui.book.read.page.entities
data class TextChar(
val charData: String,
var selected: Boolean = false,
val leftBottomPosition: TextPoint,
val rightTopPosition: TextPoint
var start: Float,
var end: Float,
var selected: Boolean = false
)

@ -3,8 +3,26 @@ package io.legado.app.ui.book.read.page.entities
data class TextLine(
var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(),
var lineTop: Int = 0,
var lineBottom: Int = 0,
var lineTop: Float = 0f,
var lineBase: Float = 0f,
var lineBottom: Float = 0f,
val isTitle: Boolean = false,
var isReadAloud: Boolean = false
)
) {
fun addTextChar(charData: String, start: Float, end: Float) {
textChars.add(TextChar(charData, start = start, end = end))
}
fun getTextCharAt(index: Int): TextChar {
return textChars[index]
}
fun getTextCharReverseAt(index: Int): TextChar {
return textChars[textChars.lastIndex - index]
}
fun getTextCharsCount(): Int {
return textChars.size
}
}

@ -1,7 +1,10 @@
package io.legado.app.ui.book.read.page.entities
import android.text.Layout
import android.text.StaticLayout
import io.legado.app.App
import io.legado.app.R
import io.legado.app.ui.book.read.page.ChapterProvider
data class TextPage(
var index: Int = 0,
@ -10,9 +13,44 @@ data class TextPage(
val textLines: ArrayList<TextLine> = arrayListOf(),
var pageSize: Int = 0,
var chapterSize: Int = 0,
var chapterIndex: Int = 0
var chapterIndex: Int = 0,
var height: Int = 0
) {
@Suppress("DEPRECATION")
fun format(): TextPage {
if (textLines.isEmpty() && ChapterProvider.visibleWidth > 0) {
val layout = StaticLayout(
text, ChapterProvider.contentPaint, ChapterProvider.visibleWidth,
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
)
var y = (ChapterProvider.visibleHeight - layout.height) / 2f
if (y < 0) y = 0f
for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine()
textLine.lineTop = (ChapterProvider.paddingTop + y -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)))
textLine.lineBase = (ChapterProvider.paddingTop + y -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)))
textLine.lineBottom =
textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent
var x = (ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2
textLine.text =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
for (i in textLine.text.indices) {
val char = textLine.text[i].toString()
val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
val x1 = x + cw
textLine.textChars.add(TextChar(charData = char, start = x, end = x1))
x = x1
}
textLines.add(textLine)
}
height = ChapterProvider.visibleHeight
}
return this
}
fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine ->
textLine.isReadAloud = false

@ -1,6 +0,0 @@
package io.legado.app.ui.book.read.page.entities
data class TextPoint(
val x: Float,
val y: Int
)

@ -5,11 +5,11 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class BookAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_text) {
SimpleRecyclerAdapter<Book>(context, R.layout.item_fillet_text) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {

@ -16,10 +16,6 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return true
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
if (oldItem.name != newItem.name) {
@ -28,7 +24,13 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) {
return false
}
if (oldItem.origins?.size != newItem.origins?.size) {
return true
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
if (oldItem.origins.size != newItem.origins.size) {
return false
}
if (oldItem.coverUrl != newItem.coverUrl) {
@ -56,20 +58,20 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) {
payload.putString("author", newItem.author)
}
if (oldItem.origins?.size != newItem.origins?.size) {
payload.putInt("origins", newItem.origins?.size ?: 1)
if (oldItem.origins.size != newItem.origins.size) {
payload.putInt("origins", newItem.origins.size)
}
if (oldItem.coverUrl != newItem.coverUrl) {
payload.putString("group", newItem.coverUrl)
payload.putString("cover", newItem.coverUrl)
}
if (oldItem.kind != newItem.kind) {
payload.putString("enabled", newItem.kind)
payload.putString("kind", newItem.kind)
}
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
payload.putString("enabled", newItem.latestChapterTitle)
payload.putString("last", newItem.latestChapterTitle)
}
if (oldItem.intro != newItem.intro) {
payload.putString("enabled", newItem.intro)
payload.putString("intro", newItem.intro)
}
return payload
}

@ -6,7 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.ui.widget.anima.explosion_field.ExplosionField
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -15,7 +15,7 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) :
SimpleRecyclerAdapter<SearchKeyword>(activity, R.layout.item_text) {
SimpleRecyclerAdapter<SearchKeyword>(activity, R.layout.item_fillet_text) {
private val explosionField = ExplosionField.attach2Window(activity)

@ -40,7 +40,7 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
with(itemView) {
tv_name.text = searchBook.name
tv_author.text = context.getString(R.string.author_show, searchBook.author)
bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1)
bv_originCount.setBadgeCount(searchBook.origins.size)
upLasted(itemView, searchBook.latestChapterTitle)
tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro)
upKind(itemView, searchBook.getKindList())
@ -56,9 +56,9 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
"name" -> tv_name.text = searchBook.name
"author" -> tv_author.text =
context.getString(R.string.author_show, searchBook.author)
"originCount" -> bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1)
"lasted" -> upLasted(itemView, searchBook.latestChapterTitle)
"introduce" -> tv_introduce.text =
"origins" -> bv_originCount.setBadgeCount(searchBook.origins.size)
"last" -> upLasted(itemView, searchBook.latestChapterTitle)
"intro" -> tv_introduce.text =
context.getString(R.string.intro_show, searchBook.intro)
"kind" -> upKind(itemView, searchBook.getKindList())
"cover" -> iv_cover.load(

@ -85,8 +85,8 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
private fun precisionSearch(searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook ->
if (searchBook.name.equals(searchKey, true)
|| searchBook.author.equals(searchKey, true)
if (searchBook.name.contains(searchKey, true)
|| searchBook.author.contains(searchKey, true)
) books.add(searchBook)
}
App.db.searchBookDao().insert(*books.toTypedArray())
@ -130,9 +130,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
}
}
} else if (searchKey == item.author) {
for ((i, searchBook) in copyDataS.withIndex()) {
for ((index, searchBook) in copyDataS.withIndex()) {
if (searchKey != searchBook.name && searchKey == searchBook.author) {
copyDataS.add(i, item)
copyDataS.add(index, item)
break
}
}
@ -141,6 +141,31 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
}
}
}
searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) {
1
} else if (o1.name != searchKey && o2.name == searchKey) {
-1
} else if (o1.author == searchKey && o2.author != searchKey) {
1
} else if (o1.author != searchKey && o2.author == searchKey) {
-1
} else if (o1.name == o2.name) {
when {
o1.origins.size > o2.origins.size -> {
1
}
o1.origins.size < o2.origins.size -> {
-1
}
else -> {
0
}
}
} else {
0
}
})
searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS)
}

@ -25,9 +25,13 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.*
import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.share
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs
@ -84,9 +88,7 @@ class BookSourceEditActivity :
}
}
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getSource())?.let { sourceStr ->
shareText(getString(R.string.share_book_source), sourceStr)
}
R.id.menu_share_str -> GSON.toJson(getSource())?.let { share(it) }
R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_book_source), sourceStr)
}
@ -353,18 +355,16 @@ class BookSourceEditActivity :
}
private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return }
mSoftKeyboardTool?.let {
if (it.isShowing) return
if (!isFinishing) {
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
}
}
private fun closePopupWindow() {
mSoftKeyboardTool?.let {
if (it.isShowing) {
it.dismiss()
}
}
mSoftKeyboardTool?.dismiss()
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {

@ -30,6 +30,7 @@ import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_source.*
@ -109,7 +110,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = BookSourceAdapter(this, this)
recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()

@ -22,7 +22,11 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.utils.*
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>()

@ -18,8 +18,8 @@ import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel
import io.legado.app.utils.putPrefBoolean
import kotlinx.android.synthetic.main.dialog_change_source.*
@ -92,7 +92,7 @@ class ChangeSourceDialog : DialogFragment(),
private fun initRecyclerView() {
adapter = ChangeSourceAdapter(requireContext(), this)
recycler_view.layoutManager = LinearLayoutManager(context)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {

@ -58,7 +58,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.searchBookDao().insert(searchBook)
if (screenKey.isEmpty()) {
searchBooks.add(searchBook)
} else if (searchBook.originName.contains(screenKey)) {
} else if (searchBook.name.contains(screenKey)) {
searchBooks.add(searchBook)
}
upAdapter()

@ -14,7 +14,7 @@ import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.Bookmark
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.getVerticalDivider
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModelOfActivity
import kotlinx.android.synthetic.main.fragment_bookmark.*
@ -38,7 +38,7 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
ATH.applyEdgeEffectColor(recycler_view)
adapter = BookmarkAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
}

@ -15,7 +15,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
import io.legado.app.utils.getVerticalDivider
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_chapter_list.*
@ -48,7 +48,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
adapter = ChapterListAdapter(requireContext(), this)
mLayoutManager = UpLinearLayoutManager(requireContext())
recycler_view.layoutManager = mLayoutManager
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
}

@ -26,7 +26,7 @@ import kotlinx.coroutines.Dispatchers.Main
import org.jetbrains.anko.toast
object BackupRestoreUi {
private const val selectFolderRequestCode = 21
private const val backupSelectRequestCode = 22
private const val restoreSelectRequestCode = 33
private const val oldDataRequestCode = 11
@ -34,7 +34,7 @@ object BackupRestoreUi {
fun backup(fragment: Fragment) {
val backupPath = AppConfig.backupPath
if (backupPath.isNullOrEmpty()) {
selectBackupFolder(fragment)
selectBackupFolder(fragment, backupSelectRequestCode)
} else {
if (backupPath.isContentPath()) {
val uri = Uri.parse(backupPath)
@ -46,19 +46,26 @@ object BackupRestoreUi {
fragment.toast(R.string.backup_success)
}
} else {
selectBackupFolder(fragment)
selectBackupFolder(fragment, backupSelectRequestCode)
}
} else {
backupUsePermission(fragment)
backupUsePermission(fragment, requestCode = backupSelectRequestCode)
}
}
}
private fun backupUsePermission(fragment: Fragment, path: String = Backup.legadoPath) {
private fun backupUsePermission(
fragment: Fragment,
path: String = Backup.legadoPath,
requestCode: Int = selectFolderRequestCode
) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
when (requestCode) {
selectFolderRequestCode -> AppConfig.backupPath = Backup.legadoPath
else -> {
Coroutine.async {
AppConfig.backupPath = Backup.legadoPath
Backup.backup(fragment.requireContext(), path)
@ -66,20 +73,22 @@ object BackupRestoreUi {
fragment.toast(R.string.backup_success)
}
}
}
}
.request()
}
fun selectBackupFolder(fragment: Fragment) {
fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) {
fragment.alert {
titleResource = R.string.select_folder
items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) {
0 -> backupUsePermission(fragment)
0 -> backupUsePermission(fragment, requestCode = requestCode)
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fragment.startActivityForResult(intent, backupSelectRequestCode)
fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
fragment.toast(e.localizedMessage ?: "ERROR")
@ -88,7 +97,7 @@ object BackupRestoreUi {
2 -> {
FileChooserDialog.show(
fragment.childFragmentManager,
backupSelectRequestCode,
requestCode,
mode = FileChooserDialog.DIRECTORY
)
}
@ -231,6 +240,9 @@ object BackupRestoreUi {
App.INSTANCE.toast(R.string.restore_success)
}
}
selectFolderRequestCode -> {
AppConfig.backupPath = currentPath
}
}
}
@ -240,7 +252,8 @@ object BackupRestoreUi {
data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
@ -254,7 +267,8 @@ object BackupRestoreUi {
data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
@ -264,6 +278,16 @@ object BackupRestoreUi {
}
}
}
selectFolderRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
}
}
oldDataRequestCode ->
if (resultCode == RESULT_OK) data?.data?.let { uri ->
ImportOldData.importUri(uri)

@ -141,6 +141,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
AppConfig.isNightTheme = true
}
}
putPrefInt("defaultTheme", which)
App.INSTANCE.applyDayNight()
recreateActivities()
}

@ -10,7 +10,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook
import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.widget.recycler.LoadMoreView
import io.legado.app.utils.getVerticalDivider
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_explore_show.*
import org.jetbrains.anko.startActivity
@ -34,7 +34,7 @@ class ExploreShowActivity : VMBaseActivity<ExploreShowViewModel>(R.layout.activi
private fun initRecyclerView() {
adapter = ExploreShowAdapter(this, this)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
recycler_view.adapter = adapter
loadMoreView = LoadMoreView(this)
adapter.addFooterView(loadMoreView)

@ -15,7 +15,11 @@ import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.ui.filechooser.adapter.FileAdapter
import io.legado.app.ui.filechooser.adapter.PathAdapter
import io.legado.app.utils.*
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.FileUtils
import io.legado.app.utils.applyTint
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_file_chooser.*
@ -133,7 +137,7 @@ class FileChooserDialog : DialogFragment(),
fileAdapter = FileAdapter(requireContext(), this)
pathAdapter = PathAdapter(requireContext(), this)
rv_file.addItemDecoration(rv_file.getVerticalDivider())
rv_file.addItemDecoration(VerticalDivider(requireContext()))
rv_file.layoutManager = LinearLayoutManager(activity)
rv_file.adapter = fileAdapter

@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -17,7 +18,10 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.*
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.arrange.ArrangeBookActivity
@ -27,6 +31,7 @@ import io.legado.app.ui.download.DownloadActivity
import io.legado.app.ui.importbook.ImportBookActivity
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.fragment_bookshelf.*
import kotlinx.android.synthetic.main.view_tab_layout.*
@ -61,7 +66,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
super.onCompatOptionsItemSelected(item)
when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_bookshelf_layout -> selectBookshelfLayout()
R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>()
@ -145,15 +150,36 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
}
private fun selectBookshelfLayout() {
selector(
title = "选择书架布局",
items = resources.getStringArray(R.array.bookshelf_layout).toList()
) { _, index ->
putPrefInt(PreferKey.bookshelfLayout, index)
@SuppressLint("InflateParams")
private fun configBookshelf() {
requireContext().alert(titleResource = R.string.bookshelf_layout) {
val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
val bookshelfSort = getPrefInt(PreferKey.bookshelfSort)
val root = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_bookshelf_config, null).apply {
rg_layout.checkByIndex(bookshelfLayout)
rg_sort.checkByIndex(bookshelfSort)
}
customView = root
okButton {
root.apply {
var changed = false
if (bookshelfLayout != rg_layout.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfLayout, rg_layout.getCheckedIndex())
changed = true
}
if (bookshelfSort != rg_sort.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfSort, rg_sort.getCheckedIndex())
changed = true
}
if (changed) {
activity?.recreate()
}
}
}
noButton()
}.show().applyTint()
}
@SuppressLint("InflateParams")
private fun addBookByUrl() {

@ -103,10 +103,16 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
-3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId)
}
bookshelfLiveData?.observe(this, Observer {
bookshelfLiveData?.observe(this, Observer { list ->
val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
}
val diffResult = DiffUtil
.calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), it))
booksAdapter.setItems(it, diffResult)
.calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), books))
booksAdapter.setItems(books, diffResult)
})
}

@ -14,8 +14,8 @@ import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.item_find_book.view.*
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
@ -46,7 +46,7 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
gl_child.removeAllViews()
kindList.map { kind ->
val tv = LayoutInflater.from(context)
.inflate(R.layout.item_text, gl_child, false)
.inflate(R.layout.item_fillet_text, gl_child, false)
gl_child.addView(tv)
tv.text_view.text = kind.title
tv.text_view.onClick {

@ -2,6 +2,7 @@ package io.legado.app.ui.qrcode
import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -11,7 +12,7 @@ import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.utils.RealPathUtil
import io.legado.app.utils.readBytes
import kotlinx.android.synthetic.main.activity_qrcode_capture.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast
@ -98,9 +99,11 @@ class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeVie
zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别
if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) {
val picturePath = RealPathUtil.getPath(this, it)
// 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调
zxingview.decodeQRCode(picturePath)
it.readBytes(this)?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
zxingview.decodeQRCode(bitmap)
}
}
}
}

@ -22,7 +22,11 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.utils.*
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>()

@ -28,6 +28,7 @@ import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.replacerule.edit.ReplaceEditDialog
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_replace_rule.*
@ -54,6 +55,7 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
private var dataInit = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView()
initSearchView()
initSelectActionView()
@ -72,12 +74,28 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
return super.onPrepareOptionsMenu(menu)
}
private fun initUriScheme() {
intent.data?.let {
when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else -> {
toast("格式不对")
}
}
}
}
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
adapter = ReplaceRuleAdapter(this, this)
recycler_view.adapter = adapter
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true
@ -306,9 +324,7 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
}
override fun edit(rule: ReplaceRule) {
ReplaceEditDialog
.newInstance(rule.id)
.show(supportFragmentManager, "editReplace")
ReplaceEditDialog.show(supportFragmentManager, rule.id)
}
override fun toTop(rule: ReplaceRule) {

@ -9,6 +9,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.constant.Theme
@ -22,14 +23,13 @@ class ReplaceEditDialog : DialogFragment(),
companion object {
fun newInstance(id: Long? = null): ReplaceEditDialog {
fun show(fragmentManager: FragmentManager, id: Long = -1, pattern: String? = null) {
val dialog = ReplaceEditDialog()
id?.let {
val bundle = Bundle()
bundle.putLong("id", id)
bundle.putString("pattern", pattern)
dialog.arguments = bundle
}
return dialog
dialog.show(fragmentManager, "editReplace")
}
}
@ -56,7 +56,7 @@ class ReplaceEditDialog : DialogFragment(),
tool_bar.inflateMenu(R.menu.replace_edit)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
viewModel.replaceRuleData.observe(this, Observer {
viewModel.replaceRuleData.observe(viewLifecycleOwner, Observer {
upReplaceView(it)
})
arguments?.let {

@ -19,6 +19,12 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application
App.db.replaceRuleDao().findById(id)?.let {
replaceRuleData.postValue(it)
}
} else {
bundle.getString("pattern")?.let { pattern ->
replaceRuleData.postValue(
ReplaceRule(pattern = pattern)
)
}
}
}
}

@ -15,7 +15,7 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.recycler.LoadMoreView
import io.legado.app.utils.getVerticalDivider
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_rss_artivles.*
import kotlinx.android.synthetic.main.view_load_more.view.*
@ -70,7 +70,7 @@ class RssArticlesActivity : VMBaseActivity<RssArticlesViewModel>(R.layout.activi
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = RssArticlesAdapter(this, this)
recycler_view.adapter = adapter
loadMoreView = LoadMoreView(this)

@ -10,7 +10,7 @@ import io.legado.app.base.BaseActivity
import io.legado.app.data.entities.RssStar
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.utils.getVerticalDivider
import io.legado.app.ui.widget.recycler.VerticalDivider
import kotlinx.android.synthetic.main.view_refresh_recycler.*
import org.jetbrains.anko.startActivity
@ -29,7 +29,7 @@ class RssFavoritesActivity : BaseActivity(R.layout.activity_rss_favorites),
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = RssFavoritesAdapter(this, this)
recycler_view.adapter = adapter
}

@ -5,7 +5,9 @@ import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.Observer
import io.legado.app.R
@ -14,10 +16,11 @@ import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareText
import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_rss_read.*
import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.share
import org.jsoup.Jsoup
import org.jsoup.safety.Whitelist
@ -54,7 +57,7 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
when (item.itemId) {
R.id.menu_rss_star -> viewModel.favorite()
R.id.menu_share_it -> viewModel.rssArticle?.let {
shareText("链接分享", it.link)
share(it.link)
}
R.id.menu_aloud -> readAloud()
}
@ -62,10 +65,24 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
}
private fun initWebView() {
webView.webViewClient = WebViewClient()
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (request?.url?.scheme == "http" || request?.url?.scheme == "https") {
return false
}
request?.url?.let {
openUrl(it)
}
return true
}
}
webView.settings.apply {
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
domStorageEnabled = true
allowContentAccess = true
}
}

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.Gravity
@ -19,22 +20,25 @@ import io.legado.app.constant.AppConst
import io.legado.app.data.entities.RssSource
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.*
import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_rss_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import org.jetbrains.anko.*
import kotlin.math.abs
class RssSourceEditActivity :
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false),
ViewTreeObserver.OnGlobalLayoutListener,
KeyboardToolPop.CallBack {
private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false
private val qrRequestCode = 101
private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
@ -99,9 +103,10 @@ class RssSourceEditActivity :
clipboard?.setPrimaryClip(ClipData.newPlainText(null, sourceStr))
}
}
R.id.menu_qr_code_camera -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getRssSource())?.let { sourceStr ->
shareText(getString(R.string.share_rss_source), sourceStr)
share(sourceStr)
}
R.id.menu_share_qr -> GSON.toJson(getRssSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_rss_source), sourceStr)
@ -113,7 +118,7 @@ class RssSourceEditActivity :
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener())
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
}
@ -192,21 +197,18 @@ class RssSourceEditActivity :
}
private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return }
mSoftKeyboardTool?.let {
if (it.isShowing) return
if (!isFinishing) {
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
}
}
private fun closePopupWindow() {
mSoftKeyboardTool?.let {
if (it.isShowing) {
it.dismiss()
}
}
mSoftKeyboardTool?.dismiss()
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val rect = Rect()
// 获取当前页面窗口的显示范围
@ -226,5 +228,17 @@ class RssSourceEditActivity :
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
}
}
}
}
}

@ -71,4 +71,16 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
}
}
}
fun importSource(text: String, finally: (source: RssSource) -> Unit) {
execute {
val text1 = text.trim()
GSON.fromJsonObject<RssSource>(text1)?.let {
finally.invoke(it)
}
}.onError {
toast(it.localizedMessage ?: "Error")
}
}
}

@ -22,7 +22,11 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.utils.*
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>()

@ -29,6 +29,7 @@ import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_source.*
@ -49,13 +50,14 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
get() = getViewModel(RssSourceViewModel::class.java)
private val importRecordKey = "rssSourceRecordKey"
private val qrRequestCode = 101
private val importSource = 13141
private val importSource = 124
private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView()
initSearchView()
initLiveDataGroup()
@ -101,10 +103,26 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
return true
}
private fun initUriScheme() {
intent.data?.let {
when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else -> {
toast("格式不对")
}
}
}
}
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = RssSourceAdapter(this, this)
recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()

@ -34,11 +34,11 @@ class DetailSeekBar(context: Context, attrs: AttributeSet?) : FrameLayout(contex
seek_bar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0)
typedArray.recycle()
iv_seek_add.onClick {
iv_seek_plus.onClick {
seek_bar.progressAdd(1)
onChanged?.invoke(seek_bar.progress)
}
iv_seek_remove.onClick {
iv_seek_reduce.onClick {
seek_bar.progressAdd(-1)
onChanged?.invoke(seek_bar.progress)
}

@ -10,7 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.popup_keyboard_tool.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
@ -23,7 +23,7 @@ class KeyboardToolPop(
init {
@SuppressLint("InflateParams")
this.contentView = LayoutInflater.from(context).inflate(R.layout.popup_keyboard_tool, null)
contentView = LayoutInflater.from(context).inflate(R.layout.popup_keyboard_tool, null)
isTouchable = true
isOutsideTouchable = false
@ -40,7 +40,7 @@ class KeyboardToolPop(
}
inner class Adapter(context: Context) :
SimpleRecyclerAdapter<String>(context, R.layout.item_text) {
SimpleRecyclerAdapter<String>(context, R.layout.item_fillet_text) {
override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) {
with(holder.itemView) {

@ -12,6 +12,7 @@ class LabelsBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
private val unUsedViews = arrayListOf<TextView>()
private val usedViews = arrayListOf<TextView>()
var textSize = 12f
fun setLabels(labels: Array<String>) {
clear()
@ -51,6 +52,7 @@ class LabelsBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
unUsedViews.removeAt(unUsedViews.lastIndex)
}
}
tv.textSize = textSize
tv.text = label
addView(tv)
}

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

Loading…
Cancel
Save