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. 78
      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. 77
      app/src/main/java/io/legado/app/model/WebBook.kt
  26. 20
      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. 47
      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. 70
      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. 339
      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. 599
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  53. 124
      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. 162
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  57. 108
      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. 207
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  60. 130
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  61. 101
      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. 24
      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. 56
      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. 46
      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. 16
      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. 74
      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")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
} }
applicationIdSuffix '.release'
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
@ -123,7 +124,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.3' def room_version = '2.2.4'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
@ -139,7 +140,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.4.5' 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-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -175,7 +176,7 @@ dependencies {
//MarkDown //MarkDown
implementation 'ru.noties.markwon:core:3.0.2' implementation 'ru.noties.markwon:core:3.0.2'
// //
implementation 'com.github.houbb:opencc4j:1.4.0' 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" "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 <activity
android:name=".ui.config.ConfigActivity" android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTask" />
<activity <activity
android:name="io.legado.app.ui.book.search.SearchActivity" android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
@ -135,24 +132,51 @@
<activity <activity
android:name=".ui.about.DonateActivity" android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" /> 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"> <activity android:name=".ui.book.source.manage.BookSourceActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="booksource" android:host="booksource"
android:scheme="yuedu" /> android:scheme="yuedu" />
</intent-filter> </intent-filter>
</activity> </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.chapterlist.ChapterListActivity" />
<activity android:name=".ui.rss.read.ReadRssActivity" /> <activity android:name=".ui.rss.read.ReadRssActivity" />
<activity android:name=".ui.importbook.ImportBookActivity" /> <activity android:name=".ui.importbook.ImportBookActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" /> <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.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" /> <activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.rss.favorites.RssFavoritesActivity" /> <activity android:name=".ui.rss.favorites.RssFavoritesActivity" />

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

@ -2,6 +2,41 @@
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 先在旧版阅读(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** **2020/02/21**
* 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整 * 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整
* 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题 * 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题

@ -5,6 +5,7 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -62,6 +63,13 @@ class App : Application() {
registerActivityLife() 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() .apply()
} }
// ChapterProvider.upReadAloudSpan()
} }
fun applyDayNight() { fun applyDayNight() {

@ -2,6 +2,7 @@ package io.legado.app.base
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -21,4 +22,12 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
super.onDestroy() super.onDestroy()
job.cancel() 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? = fun getItem(position: Int): ITEM? =
if (position in 0 until items.size) items[position] else null 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 fun getItems(): List<ITEM> = items
protected open fun getItemViewType(item: ITEM, position: Int): Int { protected open fun getItemViewType(item: ITEM, position: Int): Int {

@ -16,6 +16,7 @@ object PreferKey {
const val nextKey = "nextKeyCode" const val nextKey = "nextKeyCode"
const val showRss = "showRss" const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout" const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort"
const val recordLog = "recordLog" const val recordLog = "recordLog"
const val processText = "process_text" const val processText = "process_text"
const val cleanCache = "cleanCache" const val cleanCache = "cleanCache"
@ -36,4 +37,7 @@ object PreferKey {
const val textSelectAble = "selectText" const val textSelectAble = "selectText"
const val lastBackup = "lastBackup" const val lastBackup = "lastBackup"
const val bodyIndent = "textIndent" 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") @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>> 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>> 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>> 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>> 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>> 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>> fun liveGroupExplore(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey") @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 || '%'") @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource> 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> val hasBookUrlPattern: List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''") @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") @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun liveEnabled(): LiveData<List<RssSource>> 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>> fun liveGroup(): LiveData<List<String>>
@get:Query("select min(customOrder) from rssSources") @get:Query("select min(customOrder) from rssSources")

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

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

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

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

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

@ -86,4 +86,10 @@ object AppConfig {
set(value) { set(value) {
App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, 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 bookName: String? = null
private var bookOrigin: String? = null private var bookOrigin: String? = null
private var replaceRules: List<ReplaceRule> = arrayListOf() private var replaceRules: List<ReplaceRule> = arrayListOf()
val bodyIndent var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
get() = " ".repeat(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( fun disposeContent(
title: String, title: String,
@ -247,7 +253,6 @@ object BookHelp {
content: String, content: String,
enableReplace: Boolean enableReplace: Boolean
): String { ): String {
var c = content
synchronized(this) { synchronized(this) {
if (enableReplace && (bookName != name || bookOrigin != origin)) { if (enableReplace && (bookName != name || bookOrigin != origin)) {
replaceRules = if (origin.isNullOrEmpty()) { replaceRules = if (origin.isNullOrEmpty()) {
@ -257,6 +262,7 @@ object BookHelp {
} }
} }
} }
var c = content
for (item in replaceRules) { for (item in replaceRules) {
item.pattern.let { item.pattern.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
@ -268,8 +274,8 @@ object BookHelp {
} }
} }
} }
if (!content.substringBefore("\n").contains(title)) { if (!c.substringBefore("\n").contains(title)) {
c = title + "\n" + c c = "$title\n$c"
} }
when (AppConfig.chineseConverterType) { when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(c) 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 * @return 开发者处理了操作应该返回true开发者没有处理就返回false
*/ */
fun onMove(srcPosition: Int, targetPosition: Int): Boolean { fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
return false return true
} }
fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {

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

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

@ -12,6 +12,8 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig 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 io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -104,8 +106,17 @@ object Restore {
else -> Unit else -> Unit
} }
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode) 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)) LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
} }

@ -86,17 +86,18 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<Book> { ): Coroutine<Book> {
book.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = if (!book.infoHtml.isNullOrEmpty()) { val body =
book.infoHtml if (!book.infoHtml.isNullOrEmpty()) {
} else { book.infoHtml
val analyzeUrl = AnalyzeUrl( } else {
book = book, val analyzeUrl = AnalyzeUrl(
ruleUrl = book.bookUrl, book = book,
baseUrl = sourceUrl, ruleUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap() baseUrl = sourceUrl,
) headerMapF = bookSource.getHeaderMap()
analyzeUrl.getResponseAwait().body )
} analyzeUrl.getResponseAwait().body
}
BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl) BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl)
book book
} }
@ -112,16 +113,17 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<List<BookChapter>> { ): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { val body =
book.tocHtml if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
} else { book.tocHtml
AnalyzeUrl( } else {
book = book, AnalyzeUrl(
ruleUrl = book.tocUrl, book = book,
baseUrl = book.bookUrl, ruleUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap() baseUrl = book.bookUrl,
).getResponseAwait().body headerMapF = bookSource.getHeaderMap()
} ).getResponseAwait().body
}
BookChapterList.analyzeChapterList(this, book, body, bookSource, book.tocUrl) BookChapterList.analyzeChapterList(this, book, body, bookSource, book.tocUrl)
} }
} }
@ -141,22 +143,23 @@ class WebBook(val bookSource: BookSource) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
return@async bookChapter.url return@async bookChapter.url
} }
val body = if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { val body =
book.tocHtml if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
} else { book.tocHtml
val analyzeUrl = } else {
AnalyzeUrl( val analyzeUrl =
book = book, AnalyzeUrl(
ruleUrl = bookChapter.url, book = book,
baseUrl = book.tocUrl, ruleUrl = bookChapter.url,
headerMapF = bookSource.getHeaderMap() baseUrl = book.tocUrl,
) headerMapF = bookSource.getHeaderMap()
analyzeUrl.getResponseAwait( )
bookSource.bookSourceUrl, analyzeUrl.getResponseAwait(
jsStr = bookSource.getContentRule().webJs, bookSource.bookSourceUrl,
sourceRegex = bookSource.getContentRule().sourceRegex jsStr = bookSource.getContentRule().webJs,
).body sourceRegex = bookSource.getContentRule().sourceRegex
} ).body
}
BookContent.analyzeContent( BookContent.analyzeContent(
this, this,
body, body,

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

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

@ -33,11 +33,15 @@ object BookContent {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val analyzeRule = AnalyzeRule(book)
val content = StringBuilder() val content = StringBuilder()
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule() val contentRule = bookSource.getContentRule()
var contentData = analyzeContent(body, contentRule, book, bookChapter, bookSource, baseUrl) var contentData = analyzeContent(
content.append(contentData.content) analyzeRule.setContent(body, baseUrl),
contentRule, bookChapter, bookSource
)
content.append(contentData.content.replace(bookChapter.title, ""))
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty()) val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
@ -56,15 +60,15 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getResponseAwait() ).getResponseAwait()
.body?.let { nextBody -> .body?.let { nextBody ->
contentData = contentData =
analyzeContent( analyzeContent(
nextBody, contentRule, book, analyzeRule.setContent(nextBody, nextUrl),
bookChapter, bookSource, baseUrl, false contentRule, bookChapter, bookSource, false
) )
nextUrl = nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
content.append(contentData.content) content.append(contentData.content)
} }
} }
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) { } else if (contentData.nextUrl.size > 1) {
@ -81,19 +85,20 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getResponseAwait() ).getResponseAwait()
.body?.let { .body?.let {
contentData = contentData =
analyzeContent( analyzeContent(
it, contentRule, book, bookChapter, analyzeRule.setContent(it, item.nextUrl),
bookSource, item.nextUrl, false contentRule, bookChapter, bookSource, false
) )
item.content = contentData.content item.content = contentData.content
} }
} }
} }
for (item in contentDataList) { for (item in contentDataList) {
content.append(item.content) content.append(item.content)
} }
} }
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")
@ -103,17 +108,13 @@ object BookContent {
@Throws(Exception::class) @Throws(Exception::class)
private fun analyzeContent( private fun analyzeContent(
body: String, analyzeRule: AnalyzeRule,
contentRule: ContentRule, contentRule: ContentRule,
book: Book,
chapter: BookChapter, chapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String,
printLog: Boolean = true printLog: Boolean = true
): ContentData<List<String>> { ): ContentData<List<String>> {
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.chapter = chapter analyzeRule.chapter = chapter
val nextUrlRule = contentRule.nextContentUrl val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) { if (!nextUrlRule.isNullOrEmpty()) {

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

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

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

@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
object ReadBook { object ReadBook {
@ -25,10 +26,10 @@ object ReadBook {
var book: Book? = null var book: Book? = null
var inBookshelf = false var inBookshelf = false
var chapterSize = 0 var chapterSize = 0
var callBack: CallBack? = null
var durChapterIndex = 0 var durChapterIndex = 0
var durPageIndex = 0 var durPageIndex = 0
var isLocalBook = true var isLocalBook = true
var callBack: CallBack? = null
var prevTextChapter: TextChapter? = null var prevTextChapter: TextChapter? = null
var curTextChapter: TextChapter? = null var curTextChapter: TextChapter? = null
var nextTextChapter: TextChapter? = null var nextTextChapter: TextChapter? = null
@ -126,7 +127,13 @@ object ReadBook {
saveRead() saveRead()
} }
fun curPageChanged() { fun setPageIndex(pageIndex: Int) {
durPageIndex = pageIndex
saveRead()
curPageChanged()
}
private fun curPageChanged() {
callBack?.upPageProgress() callBack?.upPageProgress()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause) 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 { interface CallBack {
fun upContent(position: Int = 0) fun upContent(relativePosition: Int = 0)
fun upView() fun upView()
fun upPageProgress() fun upPageProgress()
fun contentLoadFinish() fun contentLoadFinish()

@ -1,14 +1,12 @@
package io.legado.app.ui.about package io.legado.app.ui.about
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.utils.shareText import io.legado.app.utils.openUrl
import org.jetbrains.anko.toast import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) { class AboutActivity : BaseActivity(R.layout.activity_about) {
@ -28,22 +26,13 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_scoring -> openIntent("market://details?id=$packageName") R.id.menu_scoring -> openUrl("market://details?id=$packageName")
R.id.menu_share_it -> shareText( R.id.menu_share_it -> share(
"App Share", getString(R.string.app_share_description),
getString(R.string.app_share_description) getString(R.string.app_name)
) )
} }
return super.onCompatOptionsItemSelected(item) 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 package io.legado.app.ui.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -14,6 +11,8 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.dialog.TextDialog 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 import io.legado.app.utils.toast
class AboutFragment : PreferenceFragmentCompat() { class AboutFragment : PreferenceFragmentCompat() {
@ -43,32 +42,32 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) { when (preference?.key) {
"contributors" -> openIntent(Intent.ACTION_VIEW, R.string.contributors_url) "contributors" -> openUrl(R.string.contributors_url)
"update_log" -> showUpdateLog() "update_log" -> showUpdateLog()
"check_update" -> openIntent(Intent.ACTION_VIEW, R.string.latest_release_url) "check_update" -> openUrl(R.string.latest_release_url)
"mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com") "mail" -> sendMail()
"git" -> openIntent(Intent.ACTION_VIEW, R.string.this_github_url) "git" -> openUrl(R.string.this_github_url)
"home_page" -> openIntent(Intent.ACTION_VIEW, R.string.home_page_url) "home_page" -> openUrl(R.string.home_page_url)
"license" -> openIntent(Intent.ACTION_VIEW, licenseUrl) "license" -> requireContext().openUrl(licenseUrl)
"disclaimer" -> openIntent(Intent.ACTION_VIEW, disclaimerUrl) "disclaimer" -> requireContext().openUrl(disclaimerUrl)
"qq" -> showQqGroups() "qq" -> showQqGroups()
"gzGzh" -> sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
private fun openIntent(intentName: String, @StringRes addressID: Int) { private fun openUrl(@StringRes addressID: Int) {
openIntent(intentName, getString(addressID)) requireContext().openUrl(getString(addressID))
} }
private fun openIntent(intentName: String, address: String) { private fun sendMail() {
try { try {
val intent = Intent(intentName) val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse(address) intent.data = Uri.parse("mailto:kunfei.ge@gmail.com")
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
toast(R.string.can_not_open) toast(e.localizedMessage ?: "Error")
} }
} }
@ -86,7 +85,7 @@ class AboutFragment : PreferenceFragmentCompat() {
items(names) { _, index -> items(names) { _, index ->
qqGroups[names[index]]?.let { qqGroups[names[index]]?.let {
if (!joinQQGroup(it)) { 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 package io.legado.app.ui.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -11,7 +9,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.utils.ACache 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 org.jetbrains.anko.longToast
import java.net.URLEncoder import java.net.URLEncoder
@ -33,40 +32,19 @@ class DonateFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) { when (preference?.key) {
"wxZsm" -> openIntent(Intent.ACTION_VIEW, wxZsRwmUrl) "wxZsm" -> requireContext().openUrl(wxZsRwmUrl)
"zfbHbRwm" -> openIntent(Intent.ACTION_VIEW, zfbHbRwmUrl) "zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> openIntent(Intent.ACTION_VIEW, zfbSkRwmUrl) "zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> openIntent(Intent.ACTION_VIEW, qqSkRwmUrl) "qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext()) "zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext()) "zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
} }
return super.onPreferenceTreeClick(preference) 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) { private fun getZfbHb(context: Context) {
sendToClip("537954522") requireContext().sendToClip("537954522")
context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包") context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包")
try { try {
val packageManager = context.applicationContext.packageManager 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.GroupManageDialog
import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.widget.SelectActionBar 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.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.* import kotlinx.android.synthetic.main.activity_arrange_book.*
@ -60,7 +60,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this) adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
select_action_bar.setMainActionText(R.string.move_to_group) select_action_bar.setMainActionText(R.string.move_to_group)
@ -95,12 +95,14 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initBookData() { private fun initBookData() {
booksLiveData?.removeObservers(this) booksLiveData?.removeObservers(this)
booksLiveData = booksLiveData =
if (groupId == -1) { when (groupId) {
App.db.bookDao().observeAll() -1 -> App.db.bookDao().observeAll()
} else { -2 -> App.db.bookDao().observeLocal()
App.db.bookDao().observeByGroup(groupId) -3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { booksLiveData?.observe(this, Observer {
adapter.selectedBooks.clear()
adapter.setItems(it) adapter.setItems(it)
upSelectCount() 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.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
@ -77,7 +77,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
.isChecked = AppConst.bookGroupAudioShow .isChecked = AppConst.bookGroupAudioShow
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult = 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.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_book_group_picker.* 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 tool_bar.menu.findItem(R.id.menu_group_audio).isVisible = false
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback() val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter itemTouchCallback.onItemTouchCallbackListener = adapter

@ -10,7 +10,6 @@ import android.view.View.NO_ID
import android.widget.EditText import android.widget.EditText
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig 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.Download
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.requestInputMethod import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_download_choice.view.* import kotlinx.android.synthetic.main.dialog_download_choice.view.*
import kotlinx.android.synthetic.main.dialog_edit_text.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_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
val hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) if (ReadBookConfig.hideNavigationBar) {
if (hideNavigationBar) {
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
} }
if (toolBarHide) { if (toolBarHide) {
if (App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)) { if (ReadBookConfig.hideStatusBar) {
flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN
} }
if (hideNavigationBar) { if (ReadBookConfig.hideNavigationBar) {
flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
} }
} }

@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.* import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.size 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.noButton
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.accentColor 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.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook 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.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR 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.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.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.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.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity 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), class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read),
View.OnTouchListener, View.OnTouchListener,
PageView.CallBack, PageView.CallBack,
TextActionMenu.CallBack,
ContentTextView.CallBack,
ReadMenu.CallBack, ReadMenu.CallBack,
ReadAloudDialog.CallBack, ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack, ChangeSourceDialog.CallBack,
ReadBook.CallBack, ReadBook.CallBack,
TocRegexDialog.CallBack,
ColorPickerDialogListener { ColorPickerDialogListener {
private val requestCodeChapterList = 568 private val requestCodeChapterList = 568
private val requestCodeEditSource = 111 private val requestCodeEditSource = 111
private val requestCodeReplace = 312 private val requestCodeReplace = 312
private var menu: Menu? = null private var menu: Menu? = null
private var textActionMenu: TextActionMenu? = null
override val viewModel: ReadBookViewModel override val viewModel: ReadBookViewModel
get() = getViewModel(ReadBookViewModel::class.java) get() = getViewModel(ReadBookViewModel::class.java)
override val isInitFinish: Boolean override val isInitFinish: Boolean get() = viewModel.isInitFinish
get() = viewModel.isInitFinish
private val mHandler = Handler() private val mHandler = Handler()
private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) } private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) }
private var screenTimeOut: Long = 0 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
Help.upLayoutInDisplayCutoutMode(window) Help.upLayoutInDisplayCutoutMode(window)
@ -98,15 +104,15 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
upSystemUiVisibility() upSystemUiVisibility()
timeElectricityReceiver = TimeElectricityReceiver.register(this) timeBatteryReceiver = TimeBatteryReceiver.register(this)
page_view.upTime() page_view.upTime()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
timeElectricityReceiver?.let { timeBatteryReceiver?.let {
unregisterReceiver(it) unregisterReceiver(it)
timeElectricityReceiver = null timeBatteryReceiver = null
} }
upSystemUiVisibility() upSystemUiVisibility()
} }
@ -209,6 +215,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_book_info -> ReadBook.book?.let { R.id.menu_book_info -> ReadBook.book?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl)) startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl))
} }
R.id.menu_toc_regex -> TocRegexDialog.show(
supportFragmentManager,
ReadBook.book?.tocUrl
)
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
@ -318,14 +328,98 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (event.action) { when (event.action) {
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> { MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
when (v.id) { when (v.id) {
R.id.cursor_left -> page_view.selectStartMove(event.x, event.y) R.id.cursor_left -> page_view.curPage.selectStartMove(
R.id.cursor_right -> page_view.selectEndMove(event.x, event.y) 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 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 { launch {
page_view.upContent(position) page_view.upContent(relativePosition)
} }
} }
/**
* 更新视图
*/
override fun upView() { override fun upView() {
launch { launch {
ReadBook.curTextChapter?.let { ReadBook.curTextChapter?.let {
@ -380,12 +480,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
} }
/**
* 更新进度条
*/
override fun upPageProgress() { override fun upPageProgress() {
launch { launch {
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
} }
} }
/**
* 显示菜单
*/
override fun showMenuBar() { override fun showMenuBar() {
read_menu.runMenuIn() read_menu.runMenuIn()
} }
@ -397,12 +503,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
viewModel.changeTo(book) viewModel.changeTo(book)
} }
override fun setPageIndex(pageIndex: Int) {
ReadBook.durPageIndex = pageIndex
ReadBook.saveRead()
ReadBook.curPageChanged()
}
override fun clickCenter() { override fun clickCenter() {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
showReadAloudDialog() 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() { override fun showReadAloudDialog() {
ReadAloudDialog().show(supportFragmentManager, "readAloud") ReadAloudDialog().show(supportFragmentManager, "readAloud")
} }
/**
* 自动翻页
*/
override fun autoPage() { override fun autoPage() {
} }
@ -495,6 +592,13 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/ */
override fun onDialogDismissed(dialogId: Int) = Unit 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
@ -528,6 +632,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss()
page_view.onDestroy()
} }
override fun observeLiveBus() { override fun observeLiveBus() {
@ -549,7 +655,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility() upSystemUiVisibility()
page_view.upBg() page_view.upBg()
page_view.upStyle() page_view.upStyle()
ChapterProvider.upStyle(ReadBookConfig.durConfig)
if (it) { if (it) {
ReadBook.loadContent() ReadBook.loadContent()
} else { } else {
@ -586,7 +691,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upScreenTimeOut() upScreenTimeOut()
} }
observeEvent<Boolean>(PreferKey.textSelectAble) { 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) Help.keepScreenOn(window, true)
return return
} }
val t = screenTimeOut - getScreenOffTime() val t = screenTimeOut - sysScreenOffTime
if (t > 0) { if (t > 0) {
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
Help.keepScreenOn(window, true) Help.keepScreenOn(window, true)

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

@ -10,8 +10,8 @@ import android.widget.SeekBar
import androidx.core.view.isVisible import androidx.core.view.isVisible
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig 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.accentColor
import io.legado.app.lib.theme.buttonDisabledColor import io.legado.app.lib.theme.buttonDisabledColor
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
@ -199,9 +199,8 @@ class ReadMenu : FrameLayout {
vw_menu_bg.onClick { runMenuOut() } vw_menu_bg.onClick { runMenuOut() }
vwNavigationBar.layoutParams = vwNavigationBar.layoutParams.apply { vwNavigationBar.layoutParams = vwNavigationBar.layoutParams.apply {
height = height =
if (context.getPrefBoolean(PreferKey.hideNavigationBar) if (ReadBookConfig.hideNavigationBar && Help.isNavigationBarExist(activity))
&& Help.isNavigationBarExist(activity) context.navigationBarHeight
) context.getNavigationBarHeight()
else 0 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) { override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply { holder.itemView.apply {
this.onClick { this.onClick {
getItem(holder.layoutPosition)?.let { getItemByLayoutPosition(holder.layoutPosition)?.let {
ReadBookConfig.durConfig.setBg(1, it) ReadBookConfig.durConfig.setBg(1, it)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)

@ -14,6 +14,7 @@ import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
@ -92,8 +93,14 @@ class MoreConfigDialog : DialogFragment() {
key: String? key: String?
) { ) {
when (key) { when (key) {
PreferKey.hideStatusBar -> postEvent(EventBus.UP_CONFIG, true) PreferKey.hideStatusBar -> {
PreferKey.hideNavigationBar -> postEvent(EventBus.UP_CONFIG, true) 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.keepLight -> postEvent(key, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) 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.constant.EventBus
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help 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.postEvent
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_read_padding.* import kotlinx.android.synthetic.main.dialog_read_padding.*
class PaddingConfigDialog : DialogFragment() { class PaddingConfigDialog : DialogFragment() {
@ -49,7 +51,15 @@ class PaddingConfigDialog : DialogFragment() {
ReadBookConfig.save() 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_top.progress = paddingTop
dsb_padding_bottom.progress = paddingBottom dsb_padding_bottom.progress = paddingBottom
@ -67,7 +77,7 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_right.progress = footerPaddingRight dsb_footer_padding_right.progress = footerPaddingRight
} }
private fun initView() = with(ReadBookConfig.durConfig) { private fun initView() = with(ReadBookConfig) {
//正文 //正文
dsb_padding_top.onChanged = { dsb_padding_top.onChanged = {
paddingTop = it paddingTop = it

@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ImageLoader import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor 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.Help
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog 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.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_read_book_style.* import kotlinx.android.synthetic.main.dialog_read_book_style.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onCheckedChange
@ -28,6 +31,8 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
val callBack = activity as? ReadBookActivity
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = DisplayMetrics() val dm = DisplayMetrics()
@ -76,7 +81,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
private fun initData() { 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) { if (it >= 0 && it < rg_page_anim.childCount) {
rg_page_anim.check(rg_page_anim[it].id) rg_page_anim.check(rg_page_anim[it].id)
} }
@ -90,8 +96,15 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
chinese_converter.onChanged { chinese_converter.onChanged {
postEvent(EventBus.UP_CONFIG, true) 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 { tv_text_bold.onClick {
ReadBookConfig.durConfig.apply { ReadBookConfig.apply {
textBold = !textBold textBold = !textBold
tv_text_bold.isSelected = textBold tv_text_bold.isSelected = textBold
} }
@ -105,43 +118,41 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
title = getString(R.string.text_indent), title = getString(R.string.text_indent),
items = resources.getStringArray(R.array.indent).toList() items = resources.getStringArray(R.array.indent).toList()
) { _, index -> ) { _, index ->
putPrefInt(PreferKey.bodyIndent, index) BookHelp.bodyIndentCount = index
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }
tv_padding.onClick { tv_padding.onClick {
val activity = activity
dismiss() dismiss()
if (activity is ReadBookActivity) { callBack?.showPaddingConfig()
activity.showPaddingConfig()
}
} }
dsb_text_size.onChanged = { dsb_text_size.onChanged = {
ReadBookConfig.durConfig.textSize = it + 5 ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
dsb_text_letter_spacing.onChanged = { dsb_text_letter_spacing.onChanged = {
ReadBookConfig.durConfig.letterSpacing = (it - 50) / 100f ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
dsb_line_size.onChanged = { dsb_line_size.onChanged = {
ReadBookConfig.durConfig.lineSpacingExtra = it ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
dsb_paragraph_spacing.onChanged = { dsb_paragraph_spacing.onChanged = {
ReadBookConfig.durConfig.paragraphSpacing = it ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
rg_page_anim.onCheckedChange { _, checkedId -> rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) { rg_page_anim.getIndexById(checkedId).let {
if (checkedId == rg_page_anim[i].id) { ReadBookConfig.pageAnim = it
requireContext().putPrefInt(PreferKey.pageAnim, i) callBack?.page_view?.upPageAnim()
val activity = activity }
if (activity is ReadBookActivity) { }
activity.page_view.upPageAnim(i) cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
} if (checkBox.isPressed) {
break ReadBookConfig.shareLayout = isChecked
} upStyle()
postEvent(EventBus.UP_CONFIG, true)
} }
} }
bg0.onClick { changeBg(0) } bg0.onClick { changeBg(0) }
@ -169,15 +180,13 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun showBgTextConfig(index: Int): Boolean { private fun showBgTextConfig(index: Int): Boolean {
dismiss() dismiss()
changeBg(index) changeBg(index)
val activity = activity callBack?.showBgTextConfig()
if (activity is ReadBookActivity) {
activity.showBgTextConfig()
}
return true return true
} }
private fun upStyle() { private fun upStyle() {
ReadBookConfig.durConfig.let { ReadBookConfig.let {
tv_title_center.isSelected = it.titleCenter
tv_text_bold.isSelected = it.textBold tv_text_bold.isSelected = it.textBold
dsb_text_size.progress = it.textSize - 5 dsb_text_size.progress = it.textSize - 5
dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50 dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50
@ -200,12 +209,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
4 -> bg4 4 -> bg4
else -> bg0 else -> bg0
} }
ReadBookConfig.getConfig(i).apply { iv.setImageDrawable(ReadBookConfig.getConfig(i).bgDrawable(100, 150))
when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().into(iv)
else -> iv.setImageDrawable(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.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.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.dp
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.removePref import io.legado.app.utils.removePref
@ -20,10 +23,12 @@ import io.legado.app.utils.removePref
object ChapterProvider { object ChapterProvider {
var viewWidth = 0 var viewWidth = 0
var viewHeight = 0 var viewHeight = 0
private var visibleWidth = 0 var paddingLeft = 0
private var visibleHeight = 0 var paddingTop = 0
private var paddingLeft = 0 var visibleWidth = 0
private var paddingTop = 0 var visibleHeight = 0
var visibleRight = 0
var visibleBottom = 0
private var lineSpacingExtra = 0f private var lineSpacingExtra = 0f
private var paragraphSpacing = 0 private var paragraphSpacing = 0
var typeface: Typeface = Typeface.SANS_SERIF var typeface: Typeface = Typeface.SANS_SERIF
@ -32,50 +37,12 @@ object ChapterProvider {
private var bodyIndent = BookHelp.bodyIndent private var bodyIndent = BookHelp.bodyIndent
init { init {
upStyle(ReadBookConfig.durConfig) upStyle()
}
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
} }
/**
* 获取拆分完的章节数据
*/
fun getTextChapter( fun getTextChapter(
bookChapter: BookChapter, bookChapter: BookChapter,
content: String, content: String,
@ -111,6 +78,8 @@ object ChapterProvider {
durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder) durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder)
} }
} }
textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString()
if (pageLines.size < textPages.size) { if (pageLines.size < textPages.size) {
pageLines.add(textPages.last().textLines.size) pageLines.add(textPages.last().textLines.size)
} }
@ -152,61 +121,39 @@ object ChapterProvider {
Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true
) )
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {
textPages.last().height = durY
durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
val textLine = TextLine(isTitle = true) val textLine = TextLine(isTitle = true)
if (durY < visibleHeight) { if (durY < visibleHeight) {
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} else { } else {
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.last().text = stringBuilder.toString() textPages.last().text = stringBuilder.toString()
stringBuilder.clear() stringBuilder.clear()
pageLines.add(textPages.last().textLines.size) pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length) pageLengths.add(textPages.last().text.length)
//新页面
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.add(TextPage()) textPages.add(TextPage())
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} }
textLine.lineBottom = paddingTop + durY - textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)) (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
textLine.lineTop = paddingTop + durY - textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)) (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + titlePaint.fontMetrics.descent
val words = val words =
title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words) stringBuilder.append(words)
textLine.text = words textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex) val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex != layout.lineCount - 1) { if (lineIndex != layout.lineCount - 1) {
val gapCount: Int = words.length - 1 addCharsToLineMiddle(textLine, words, titlePaint, desiredWidth, 0f)
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
}
} else { } else {
//最后一行 //最后一行
textLine.text = "$words\n" val x = if (ReadBookConfig.titleCenter)
stringBuilder.append("\n") (visibleWidth - layout.getLineWidth(lineIndex)) / 2
var x = 0f else 0f
for (i in words.indices) { addCharsToLineLast(textLine, words, stringBuilder, titlePaint, x)
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
}
} }
} }
durY += paragraphSpacing durY += paragraphSpacing
@ -230,92 +177,192 @@ object ChapterProvider {
Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true
) )
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine(isTitle = false) textPages.last().height = durY
durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
val textLine = TextLine()
if (durY < visibleHeight) { if (durY < visibleHeight) {
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} else { } else {
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.last().text = stringBuilder.toString() textPages.last().text = stringBuilder.toString()
stringBuilder.clear() stringBuilder.clear()
pageLines.add(textPages.last().textLines.size) pageLines.add(textPages.last().textLines.size)
pageLengths.add(textPages.last().text.length) pageLengths.add(textPages.last().text.length)
//新页面
durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)
textPages.add(TextPage()) textPages.add(TextPage())
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} }
textLine.lineBottom = paddingTop + durY - textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)) (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
textLine.lineTop = paddingTop + durY - textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)) (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
var words = textLine.lineBottom = textLine.lineBase + contentPaint.fontMetrics.descent
val words =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words) stringBuilder.append(words)
textLine.text = words textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex) val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex == 0 && layout.lineCount > 1) { if (lineIndex == 0 && layout.lineCount > 1) {
//第一行 //第一行
var x = 0f addCharsToLineFirst(textLine, words, contentPaint, desiredWidth)
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)
)
textLine.textChars.add(textChar)
x = x1
words = words.replaceFirst(bodyIndent, "")
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
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)
)
textLine.textChars.add(textChar1)
x = x1
}
} else if (lineIndex == layout.lineCount - 1) { } else if (lineIndex == layout.lineCount - 1) {
//最后一行 //最后一行
stringBuilder.append("\n") addCharsToLineLast(textLine, words, stringBuilder, contentPaint, 0f)
textLine.text = "$words\n"
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
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
}
} else { } else {
//中间行 //中间行
val gapCount: Int = words.length - 1 addCharsToLineMiddle(textLine, words, contentPaint, desiredWidth, 0f)
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
}
} }
} }
durY += paragraphSpacing durY += paragraphSpacing
return durY return durY
} }
/**
* 有缩进,两端对齐
*/
private fun addCharsToLineFirst(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float
) {
var x = 0f
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
)
x = x1
}
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]
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
)
x = x1
}
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 = startX
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, textPaint)
val x1 = x + cw
textLine.addTextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
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 {
when (AppConfig.systemTypefaces) {
1 -> Typeface.SERIF
2 -> Typeface.MONOSPACE
else -> Typeface.SANS_SERIF
}
}
} catch (e: Exception) {
App.INSTANCE.removePref(PreferKey.readBookFont)
Typeface.SANS_SERIF
}
//标题
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 package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View import android.view.View
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor 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.ui.book.read.page.entities.TextPage
import io.legado.app.utils.activity
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { 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 { private val selectedPaint by lazy {
Paint().apply { Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2) color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL style = Paint.Style.FILL
} }
} }
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) private var callBack: CallBack
var selectStartLine = 0 private val visibleRect = RectF()
var selectStartChar = 0 private val selectStart = arrayOf(0, 0, 0)
var selectEndLine = 0 private val selectEnd = arrayOf(0, 0, 0)
var selectEndChar = 0 private var textPage: TextPage = TextPage()
private var textPage: TextPage? = null //滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f
private var pageOffset = 0f
init {
callBack = activity as CallBack
contentDescription = textPage.text
}
fun setContent(textPage: TextPage?) { fun setContent(textPage: TextPage) {
this.textPage = textPage this.textPage = textPage
contentDescription = textPage.text
invalidate() 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) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
ReadBookConfig.durConfig.let { ChapterProvider.viewWidth = w
ChapterProvider.viewWidth = w ChapterProvider.viewHeight = h
ChapterProvider.viewHeight = h ChapterProvider.upSize()
ChapterProvider.upSize(ReadBookConfig.durConfig) upVisibleRect()
textPage.format()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.clipRect(visibleRect)
drawPage(canvas)
}
/**
* 绘制页面
*/
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
textPage.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
)
}
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
)
}
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
)
}
} }
} }
@SuppressLint("ClickableViewAccessibility") /**
override fun onTouchEvent(event: MotionEvent?): Boolean { * 绘制文字
*/
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.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()
}
return true fun resetPageOffset() {
pageOffset = 0f
} }
override fun onDraw(canvas: Canvas) { /**
super.onDraw(canvas) * 选择初始文字
textPage?.let { textPage -> */
textPage.textLines.forEach { textLine -> fun selectText(
val textPaint = if (textLine.isTitle) { x: Float,
ChapterProvider.titlePaint y: Float,
} else { select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
ChapterProvider.contentPaint ) {
if (!selectAble) return
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) {
textChar.selected = true
invalidate()
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
}
} }
textPaint.color = if (textLine.isReadAloud) { return
context.accentColor }
} else { }
ReadBookConfig.durConfig.textColor() 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
}
} }
textLine.textChars.forEach { return
canvas.drawText( }
it.charData, }
it.leftBottomPosition.x, relativeOffset = relativeOffset(2)
it.leftBottomPosition.y.toFloat(), if (relativeOffset >= ChapterProvider.visibleHeight) return
textPaint for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
) if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
if (it.selected) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
canvas.drawRect( if (x > textChar.start && x < textChar.end) {
it.leftBottomPosition.x, textChar.selected = true
it.rightTopPosition.y.toFloat(), invalidate()
it.rightTopPosition.x, selectStart[0] = 2
it.leftBottomPosition.y.toFloat(), selectStart[1] = lineIndex
selectedPaint 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
} }
} }
return
} }
} }
} }
fun selectText(x: Float, y: Float): SelectPoint? { /**
textPage?.let { textPage -> * 开始选择符移动
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { */
if (y > textLine.lineTop && y < textLine.lineBottom) { fun selectStartMove(x: Float, y: Float) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) { if (!visibleRect.contains(x, y)) return
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { var relativeOffset = relativeOffset(0)
textChar.selected = true for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
invalidate() if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
selectStartLine = lineIndex for ((charIndex, textChar) in textLine.textChars.withIndex()) {
selectStartChar = charIndex if (x > textChar.start && x < textChar.end) {
selectEndLine = lineIndex if (selectStart[0] != 0 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
selectEndChar = charIndex if (selectToInt(0, lineIndex, charIndex) > selectToInt(selectEnd)) {
return SelectPoint( return
textChar.leftBottomPosition.x, }
textChar.leftBottomPosition.y.toFloat(), selectStart[0] = 0
textChar.rightTopPosition.x, selectStart[1] = lineIndex
textChar.leftBottomPosition.y.toFloat() 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
} }
break
} }
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
} }
} }
return null
} }
fun selectStartMove(x: Float, y: Float) { /**
* 结束选择符移动
*/
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 selectEndMove(x: Float, y: Float) { /**
* 选择结束文字
*/
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.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT 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.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.ui.book.read.page.entities.TextPage
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.view_book_page.view.* import kotlinx.android.synthetic.main.view_book_page.view.*
import java.util.* import java.util.*
class ContentView : FrameLayout { class ContentView(context: Context) : FrameLayout(context) {
var callBack: CallBack? = null
private var headerHeight = 0
private var pageSize: Int = 0
constructor(context: Context) : super(context) { init {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
fun init() {
//设置背景防止切换背景时文字重叠 //设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background)) setBackgroundColor(context.getCompatColor(R.color.background))
inflate(context, R.layout.view_book_page, this) inflate(context, R.layout.view_book_page, this)
upStyle() upStyle()
upTime() upTime()
content_text_view.upView = {
tv_bottom_left.text = it.title
setPageIndex(it.index, it.pageSize)
}
} }
fun upStyle() { fun upStyle() {
ReadBookConfig.durConfig.apply { ReadBookConfig.apply {
tv_top_left.typeface = ChapterProvider.typeface tv_top_left.typeface = ChapterProvider.typeface
tv_top_right.typeface = ChapterProvider.typeface tv_top_right.typeface = ChapterProvider.typeface
tv_bottom_left.typeface = ChapterProvider.typeface tv_bottom_left.typeface = ChapterProvider.typeface
tv_bottom_right.typeface = ChapterProvider.typeface tv_bottom_right.typeface = ChapterProvider.typeface
if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { //显示状态栏时隐藏header
//显示状态栏时隐藏header if (hideStatusBar) {
ll_header.visible() ll_header.layoutParams = ll_header.layoutParams.apply {
ll_header.layoutParams = height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
ll_header.layoutParams.apply { height = context.getStatusBarHeight() } }
ll_header.setPadding( ll_header.setPadding(
headerPaddingLeft.dp, headerPaddingLeft.dp,
headerPaddingTop.dp, headerPaddingTop.dp,
headerPaddingRight.dp, headerPaddingRight.dp,
headerPaddingBottom.dp headerPaddingBottom.dp
) )
headerHeight = ll_header.height ll_header.visible()
page_panel.setPadding(0, 0, 0, 0) page_panel.setPadding(0, 0, 0, 0)
} else { } else {
ll_header.gone() ll_header.gone()
headerHeight = context.getStatusBarHeight() page_panel.setPadding(0, context.statusBarHeight, 0, 0)
page_panel.setPadding(0, headerHeight, 0, 0)
} }
content_text_view.setPadding(
paddingLeft.dp,
paddingTop.dp,
paddingRight.dp,
paddingBottom.dp
)
ll_footer.setPadding( ll_footer.setPadding(
footerPaddingLeft.dp, footerPaddingLeft.dp,
footerPaddingTop.dp, footerPaddingTop.dp,
footerPaddingRight.dp, footerPaddingRight.dp,
footerPaddingBottom.dp footerPaddingBottom.dp
) )
textColor().let { content_text_view.upVisibleRect()
durConfig.textColor().let {
tv_top_left.setTextColor(it) tv_top_left.setTextColor(it)
tv_top_right.setTextColor(it) tv_top_right.setTextColor(it)
tv_bottom_left.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?) { fun setBg(bg: Drawable?) {
page_panel.background = bg page_panel.background = bg
} }
@ -95,60 +91,64 @@ class ContentView : FrameLayout {
tv_top_right.text = context.getString(R.string.battery_show, battery) tv_top_right.text = context.getString(R.string.battery_show, battery)
} }
fun setContent(textPage: TextPage?) { fun setContent(textPage: TextPage) {
content_text_view.setContent(textPage) tv_bottom_left.text = when (AppConfig.chineseConverterType) {
if (textPage != null) { 1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
tv_bottom_left.text = textPage.title 2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
pageSize = textPage.pageSize else -> textPage.title
setPageIndex(textPage.index)
} }
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage)
}
fun resetPageOffset() {
content_text_view.resetPageOffset()
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?) { fun setPageIndex(pageIndex: Int?, pageSize: Int) {
pageIndex?.let { pageIndex?.let {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}" tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}"
} }
} }
fun onScroll(offset: Float) {
content_text_view.onScroll(offset)
}
fun upSelectAble(selectAble: Boolean) { fun upSelectAble(selectAble: Boolean) {
content_text_view.selectAble = selectAble 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 y = e.y - headerHeight
val selectPoint = content_text_view.selectText(e.x, y) return content_text_view.selectText(e.x, y, select)
selectPoint?.let {
it.startY = it.startY + headerHeight
it.endY = it.endY + headerHeight
}
return selectPoint
} }
fun selectStartMove(x: Float, y: Float) { 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) { fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectEndMove(x, y) content_text_view.selectStartMoveIndex(relativePage, lineIndex, charIndex)
} }
fun scrollTo(pos: Int?) { fun selectEndMove(x: Float, y: Float) {
if (pos != null) { content_text_view.selectEndMove(x, y - headerHeight)
content_text_view.post {
}
}
} }
fun scrollToBottom() { fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.post { content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
} }
interface CallBack { fun cancelSelect() {
fun scrollToLine(line: Int) content_text_view.cancelSelect()
fun scrollToLast()
} }
val selectedText: String get() = content_text_view.selectedText
} }

@ -1,24 +1,19 @@
package io.legado.app.ui.book.read.page 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 import io.legado.app.ui.book.read.page.entities.TextChapter
interface DataSource { 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? val prevChapter: TextChapter?
fun getNextChapter(): TextChapter?
fun getPreviousChapter(): TextChapter?
fun hasNextChapter(): Boolean fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean fun hasPrevChapter(): Boolean
} }

@ -8,13 +8,15 @@ abstract class PageFactory<DATA>(protected val dataSource: DataSource) {
abstract fun moveToNext():Boolean 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 abstract fun hasNext(): Boolean

@ -6,30 +6,26 @@ import android.graphics.Canvas
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.delegate.* 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.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt
class PageView(context: Context, attrs: AttributeSet) : class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs), FrameLayout(context, attrs),
ContentView.CallBack,
DataSource { DataSource {
var callBack: CallBack? = null var callBack: CallBack
var pageFactory: TextPageFactory? = null var pageFactory: TextPageFactory
private var pageDelegate: PageDelegate? = null var pageDelegate: PageDelegate? = null
var prevPage: ContentView var prevPage: ContentView
var curPage: ContentView var curPage: ContentView
var nextPage: ContentView var nextPage: ContentView
init { init {
callBack = activity as? CallBack callBack = activity as CallBack
prevPage = ContentView(context) prevPage = ContentView(context)
addView(prevPage) addView(prevPage)
nextPage = ContentView(context) nextPage = ContentView(context)
@ -39,8 +35,7 @@ class PageView(context: Context, attrs: AttributeSet) :
upBg() upBg()
setWillNotDraw(false) setWillNotDraw(false)
pageFactory = TextPageFactory(this) pageFactory = TextPageFactory(this)
upPageAnim(context.getPrefInt(PreferKey.pageAnim)) upPageAnim()
curPage.callBack = this
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@ -67,33 +62,34 @@ class PageView(context: Context, attrs: AttributeSet) :
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
callBack?.screenOffTimerStart() pageDelegate?.onTouch(event)
return pageDelegate?.onTouch(event) ?: super.onTouchEvent(event) callBack.screenOffTimerStart()
return true
}
fun onDestroy() {
pageDelegate?.onDestroy()
curPage.cancelSelect()
} }
fun fillPage(direction: PageDelegate.Direction) { fun fillPage(direction: PageDelegate.Direction) {
when (direction) { when (direction) {
PageDelegate.Direction.PREV -> { PageDelegate.Direction.PREV -> {
pageFactory?.moveToPrevious() pageFactory.moveToPrev()
upContent() upContent()
if (isScrollDelegate) {
curPage.scrollToBottom()
}
} }
PageDelegate.Direction.NEXT -> { PageDelegate.Direction.NEXT -> {
pageFactory?.moveToNext() pageFactory.moveToNext()
upContent() upContent()
if (isScrollDelegate) {
curPage.scrollTo(0)
}
} }
else -> Unit else -> Unit
} }
} }
fun upPageAnim(pageAnim: Int) { fun upPageAnim() {
pageDelegate?.onDestroy()
pageDelegate = null pageDelegate = null
pageDelegate = when (pageAnim) { pageDelegate = when (ReadBookConfig.pageAnim) {
0 -> CoverPageDelegate(this) 0 -> CoverPageDelegate(this)
1 -> SlidePageDelegate(this) 1 -> SlidePageDelegate(this)
2 -> SimulationPageDelegate(this) 2 -> SimulationPageDelegate(this)
@ -103,36 +99,26 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent() upContent()
} }
fun upContent(position: Int = 0) { fun upContent(relativePosition: Int = 0) {
pageFactory?.let { if (ReadBookConfig.isScroll) {
when (position) { curPage.setContent(pageFactory.currentPage)
-1 -> prevPage.setContent(it.previousPage()) } else {
1 -> nextPage.setContent(it.nextPage()) when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage)
1 -> nextPage.setContent(pageFactory.nextPage)
else -> { else -> {
curPage.setContent(it.currentPage()) curPage.setContent(pageFactory.currentPage)
nextPage.setContent(it.nextPage()) nextPage.setContent(pageFactory.nextPage)
prevPage.setContent(it.previousPage()) prevPage.setContent(pageFactory.prevPage)
if (isScrollDelegate) {
curPage.scrollTo(ReadBook.textChapter()?.getStartLine(ReadBook.durChapterPos()))
}
} }
} }
if (isScrollDelegate) {
prevPage.scrollToBottom()
}
} }
callBack?.screenOffTimerStart() callBack.screenOffTimerStart()
} }
fun moveToPrevPage(noAnim: Boolean = true) { fun moveToPrevPage(noAnim: Boolean = true) {
if (noAnim) { if (noAnim) {
if (isScrollDelegate) { fillPage(PageDelegate.Direction.PREV)
ReadBook.textChapter()?.let {
curPage.scrollTo(it.getStartLine(pageIndex - 1))
}
} else {
fillPage(PageDelegate.Direction.PREV)
}
} else { } else {
pageDelegate?.start(PageDelegate.Direction.PREV) pageDelegate?.start(PageDelegate.Direction.PREV)
} }
@ -140,24 +126,14 @@ class PageView(context: Context, attrs: AttributeSet) :
fun moveToNextPage(noAnim: Boolean = true) { fun moveToNextPage(noAnim: Boolean = true) {
if (noAnim) { if (noAnim) {
if (isScrollDelegate) { fillPage(PageDelegate.Direction.NEXT)
ReadBook.textChapter()?.let {
curPage.scrollTo(it.getStartLine(pageIndex + 1))
}
} else {
fillPage(PageDelegate.Direction.NEXT)
}
} else { } else {
pageDelegate?.start(PageDelegate.Direction.NEXT) pageDelegate?.start(PageDelegate.Direction.NEXT)
} }
} }
fun upSelectAble(selectAble: Boolean) {
pageDelegate?.upSelectAble(selectAble)
curPage.upSelectAble(selectAble)
}
fun upStyle() { fun upStyle() {
ChapterProvider.upStyle()
curPage.upStyle() curPage.upStyle()
prevPage.upStyle() prevPage.upStyle()
nextPage.upStyle() nextPage.upStyle()
@ -184,38 +160,19 @@ class PageView(context: Context, attrs: AttributeSet) :
nextPage.upBattery(battery) nextPage.upBattery(battery)
} }
fun selectStartMove(x: Float, y: Float) { override val currentChapter: TextChapter?
curPage.selectStartMove(x, y) get() {
} return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
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 { override val nextChapter: TextChapter?
return ReadBook.durChapterIndex get() {
return if (callBack.isInitFinish) ReadBook.textChapter(1) else null
} }
override fun getCurrentChapter(): TextChapter? { override val prevChapter: TextChapter?
return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null get() {
} return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null
override fun getNextChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null
}
override fun getPreviousChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null
} }
override fun hasNextChapter(): Boolean { override fun hasNextChapter(): Boolean {
@ -223,47 +180,12 @@ class PageView(context: Context, attrs: AttributeSet) :
} }
override fun hasPrevChapter(): Boolean { override fun hasPrevChapter(): Boolean {
callBack?.let { return ReadBook.durChapterIndex > 0
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 { interface CallBack {
val isInitFinish: Boolean val isInitFinish: Boolean
/**
* 保存页数
*/
fun setPageIndex(pageIndex: Int)
/**
* 点击屏幕中间
*/
fun clickCenter() fun clickCenter()
fun screenOffTimerStart() 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) { class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource) {
override fun hasPrev(): Boolean = with(dataSource) { override fun hasPrev(): Boolean = with(dataSource) {
return if (isScrollDelegate) { return hasPrevChapter() || pageIndex > 0
hasPrevChapter()
} else {
hasPrevChapter() || pageIndex > 0
}
} }
override fun hasNext(): Boolean = with(dataSource) { override fun hasNext(): Boolean = with(dataSource) {
return if (isScrollDelegate) { return hasNextChapter() || currentChapter?.isLastIndex(pageIndex) != true
hasNextChapter()
} else {
hasNextChapter()
|| getCurrentChapter()?.isLastIndex(pageIndex) != true
}
} }
override fun moveToFirst() { override fun moveToFirst() {
dataSource.setPageIndex(0) ReadBook.setPageIndex(0)
} }
override fun moveToLast() = with(dataSource) { override fun moveToLast() = with(dataSource) {
getCurrentChapter()?.let { currentChapter?.let {
if (it.pageSize() == 0) { if (it.pageSize() == 0) {
setPageIndex(0) ReadBook.setPageIndex(0)
} else { } else {
setPageIndex(it.pageSize().minus(1)) ReadBook.setPageIndex(it.pageSize().minus(1))
} }
} ?: setPageIndex(0) } ?: ReadBook.setPageIndex(0)
} }
override fun moveToNext(): Boolean = with(dataSource) { override fun moveToNext(): Boolean = with(dataSource) {
return if (hasNext()) { return if (hasNext()) {
if (getCurrentChapter()?.isLastIndex(pageIndex) == true if (currentChapter?.isLastIndex(pageIndex) == true) {
|| isScrollDelegate
) {
ReadBook.moveToNextChapter(false) ReadBook.moveToNextChapter(false)
} else { } else {
setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
true true
} else } else
false false
} }
override fun moveToPrevious(): Boolean = with(dataSource) { override fun moveToPrev(): Boolean = with(dataSource) {
return if (hasPrev()) { return if (hasPrev()) {
if (pageIndex <= 0 || isScrollDelegate) { if (pageIndex <= 0) {
ReadBook.moveToPrevChapter(false) ReadBook.moveToPrevChapter(false)
} else { } else {
setPageIndex(pageIndex.minus(1)) ReadBook.setPageIndex(pageIndex.minus(1))
} }
true true
} else } else
false false
} }
override fun currentPage(): TextPage? = with(dataSource) { override val currentPage: TextPage
return if (isScrollDelegate) { get() = with(dataSource) {
getCurrentChapter()?.scrollPage() currentChapter?.let {
} else { return@with it.page(pageIndex)
getCurrentChapter()?.page(pageIndex) ?: TextPage(title = it.title).format()
}
return TextPage().format()
} }
}
override fun nextPage(): TextPage? = with(dataSource) { override val nextPage: TextPage
if (isScrollDelegate) { get() = with(dataSource) {
return getNextChapter()?.scrollPage() currentChapter?.let {
} if (pageIndex < it.pageSize() - 1) {
getCurrentChapter()?.let { return@with it.page(pageIndex + 1)?.removePageAloudSpan()
if (pageIndex < it.pageSize() - 1) { ?: TextPage(title = it.title).format()
return getCurrentChapter()?.page(pageIndex + 1)?.removePageAloudSpan() }
}
nextChapter?.let {
return@with it.page(0)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
} }
return TextPage().format()
} }
return getNextChapter()?.page(0)?.removePageAloudSpan()
}
override fun previousPage(): TextPage? = with(dataSource) { override val prevPage: TextPage
if (isScrollDelegate) { get() = with(dataSource) {
return getPreviousChapter()?.scrollPage() if (pageIndex > 0) {
} currentChapter?.let {
if (pageIndex > 0) { return@with it.page(pageIndex - 1)?.removePageAloudSpan()
return getCurrentChapter()?.page(pageIndex - 1)?.removePageAloudSpan() ?: TextPage(title = it.title).format()
}
}
prevChapter?.let {
return@with it.lastPage()?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
return TextPage().format()
} }
return getPreviousChapter()?.lastPage()?.removePageAloudSpan()
}
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) { abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) {
override fun onScroll( override fun onTouch(event: MotionEvent) {
e1: MotionEvent, when (event.action) {
e2: MotionEvent, MotionEvent.ACTION_MOVE -> {
distanceX: Float, if (isTextSelected) {
distanceY: Float selectText(event)
): Boolean { } else {
onScroll(event)
}
}
}
super.onTouch(event)
}
private fun onScroll(event: MotionEvent) {
//判断是否移动了
if (!isMoved) { if (!isMoved) {
if (abs(distanceX) > abs(distanceY)) { isMoved = abs(startX - event.x) > slop
if (distanceX < 0) { || abs(startX - event.x) > abs(startY - event.y)
if (isMoved) {
if (event.x - startX > 0) {
//如果上一页不存在 //如果上一页不存在
if (!hasPrev()) { if (!hasPrev()) {
noNext = true noNext = true
return true return
} }
setDirection(Direction.PREV) setDirection(Direction.PREV)
setBitmap() setBitmap()
@ -26,21 +37,19 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
//如果不存在表示没有下一页了 //如果不存在表示没有下一页了
if (!hasNext()) { if (!hasNext()) {
noNext = true noNext = true
return true return
} }
setDirection(Direction.NEXT) setDirection(Direction.NEXT)
setBitmap() setBitmap()
} }
isMoved = true
} }
} }
if (isMoved) { if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) distanceX < 0 else distanceX > 0 isCancel = if (mDirection == Direction.NEXT) touchX > lastX else touchX < lastX
isRunning = true 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.graphics.RectF
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller import android.widget.Scroller
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
import kotlin.math.abs import kotlin.math.abs
@ -25,42 +24,36 @@ abstract class PageDelegate(protected val pageView: PageView) :
pageView.width * 0.66f, pageView.height * 0.66f pageView.width * 0.66f, pageView.height * 0.66f
) )
protected val context: Context = pageView.context protected val context: Context = pageView.context
protected val slop = ViewConfiguration.get(context).scaledTouchSlop
//起始点 //起始点
protected var startX: Float = 0.toFloat() protected var startX: Float = 0f
protected var startY: Float = 0.toFloat() protected var startY: Float = 0f
//上一个触碰点
protected var lastX: Float = 0f
protected var lastY: Float = 0f
//触碰点 //触碰点
protected var touchX: Float = 0.toFloat() protected var touchX: Float = 0f
protected var touchY: Float = 0.toFloat() protected var touchY: Float = 0f
protected val nextPage: ContentView protected val nextPage: ContentView get() = pageView.nextPage
get() = pageView.nextPage protected val curPage: ContentView get() = pageView.curPage
protected val prevPage: ContentView get() = pageView.prevPage
protected val curPage: ContentView
get() = pageView.curPage
protected val prevPage: ContentView
get() = pageView.prevPage
protected var bitmap: Bitmap? = null protected var bitmap: Bitmap? = null
protected var viewWidth: Int = pageView.width protected var viewWidth: Int = pageView.width
protected var viewHeight: Int = pageView.height protected var viewHeight: Int = pageView.height
//textView在顶端或低端
protected var atTop: Boolean = false
protected var atBottom: Boolean = false
private val snackBar: Snackbar by lazy { private val snackBar: Snackbar by lazy {
Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT) Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT)
} }
private val scroller: Scroller by lazy { private val scroller: Scroller by lazy {
Scroller(pageView.context, FastOutLinearInInterpolator()) Scroller(pageView.context, DecelerateInterpolator())
} }
private val detector: GestureDetector by lazy { private val detector: GestureDetector by lazy {
GestureDetector(pageView.context, this).apply { GestureDetector(pageView.context, this)
setIsLongpressEnabled(context.getPrefBoolean(PreferKey.textSelectAble))
}
} }
var isMoved = false var isMoved = false
@ -71,32 +64,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
var isCancel = false var isCancel = false
var isRunning = false var isRunning = false
var isStarted = 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) { open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x startX = x
startY = y startY = y
lastX = x
lastY = y
touchX = x
touchY = y
if (invalidate) { if (invalidate) {
invalidate() pageView.invalidate()
} }
} }
open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
lastX = touchX
lastY = touchY
touchX = x touchX = x
touchY = y touchY = y
if (invalidate) { if (invalidate) {
invalidate() pageView.invalidate()
} }
onScroll() onScroll()
} }
fun upSelectAble(selectAble: Boolean) { open fun fling(
detector.setIsLongpressEnabled(selectAble) startX: Int, startY: Int, velocityX: Int, velocityY: Int,
} minX: Int, maxX: Int, minY: Int, maxY: Int
) {
protected fun invalidate() { scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)
isRunning = true
isStarted = true
pageView.invalidate() pageView.invalidate()
} }
@ -110,28 +121,21 @@ abstract class PageDelegate(protected val pageView: PageView) :
) )
isRunning = true isRunning = true
isStarted = true isStarted = true
invalidate() pageView.invalidate()
} }
protected fun stopScroll() { private fun stopScroll() {
isRunning = false isRunning = false
isStarted = false isStarted = false
invalidate() pageView.invalidate()
if (pageView.isScrollDelegate) { bitmap?.recycle()
pageView.postDelayed({ bitmap = null
bitmap?.recycle()
bitmap = null
}, 100)
} else {
bitmap?.recycle()
bitmap = null
}
} }
fun setViewSize(width: Int, height: Int) { open fun setViewSize(width: Int, height: Int) {
viewWidth = width viewWidth = width
viewHeight = height viewHeight = height
invalidate() pageView.invalidate()
centerRectF.set( centerRectF.set(
width * 0.33f, height * 0.33f, width * 0.33f, height * 0.33f,
width * 0.66f, height * 0.66f width * 0.66f, height * 0.66f
@ -142,7 +146,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
if (scroller.computeScrollOffset()) { if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) { } else if (isStarted) {
setTouchPoint(scroller.finalX.toFloat(), scroller.finalY.toFloat(), false)
onAnimStop() onAnimStop()
stopScroll() stopScroll()
} }
@ -182,14 +185,13 @@ abstract class PageDelegate(protected val pageView: PageView) :
onAnimStart() 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 @CallSuper
open fun setDirection(direction: Direction) { open fun setDirection(direction: Direction) {
@ -208,17 +210,28 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 触摸事件处理 * 触摸事件处理
*/ */
@CallSuper @CallSuper
open fun onTouch(event: MotionEvent): Boolean { open fun onTouch(event: MotionEvent) {
if (isStarted) return false if (isStarted) return
curPage.dispatchTouchEvent(event) if (!detector.onTouchEvent(event)) {
return 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 { override fun onDown(e: MotionEvent): Boolean {
// abort() if (isTextSelected) {
curPage.cancelSelect()
isTextSelected = false
selectedOnDown = true
}
//是否移动 //是否移动
isMoved = false isMoved = false
//是否存在下一章 //是否存在下一章
@ -238,25 +251,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 单击 * 单击
*/ */
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
if (selectedOnDown) {
selectedOnDown = false
return true
}
val x = e.x val x = e.x
val y = e.y val y = e.y
if (centerRectF.contains(x, y)) { if (centerRectF.contains(x, y)) {
pageView.callBack?.clickCenter() pageView.callBack.clickCenter()
setTouchPoint(x, y) setTouchPoint(x, y)
} else { } else {
if (x > viewWidth / 2 || if (x > viewWidth / 2 ||
AppConfig.clickAllNext AppConfig.clickAllNext
) { ) {
//设置动画方向 //设置动画方向
if (!hasNext()) { if (!hasNext()) return true
return true
}
setDirection(Direction.NEXT) setDirection(Direction.NEXT)
setBitmap() setBitmap()
} else { } else {
if (!hasPrev()) { if (!hasPrev()) return true
return true
}
setDirection(Direction.PREV) setDirection(Direction.PREV)
setBitmap() setBitmap()
} }
@ -270,30 +283,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 长按选择 * 长按选择
*/ */
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent) {
val textChar = curPage.selectText(e) curPage.selectText(e) { relativePage, lineIndex, charIndex ->
textChar?.let { isTextSelected = true
pageView.callBack?.selectText(it) firstRelativePage = relativePage
firstLineIndex = lineIndex
firstCharIndex = charIndex
} }
} }
/** protected fun selectText(event: MotionEvent) {
* 移动结束 curPage.selectText(event) { relativePage, lineIndex, charIndex ->
*/ when {
override fun onFling( relativePage > firstRelativePage -> {
e1: MotionEvent?, curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
e2: MotionEvent?, curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
velocityX: Float, }
velocityY: Float relativePage < firstRelativePage -> {
): Boolean { curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
if (!noNext) onAnimStart() curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
return true }
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 { fun hasPrev(): Boolean {
val hasPrev = pageView.pageFactory?.hasPrev() == true val hasPrev = pageView.pageFactory.hasPrev()
if (!hasPrev) { if (!hasPrev) {
if (!snackBar.isShown) { if (!snackBar.isShown) {
snackBar.setText("没有上一页") snackBar.setText("没有上一页")
@ -307,7 +340,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 判断是否有下一页 * 判断是否有下一页
*/ */
fun hasNext(): Boolean { fun hasNext(): Boolean {
val hasNext = pageView.pageFactory?.hasNext() == true val hasNext = pageView.pageFactory.hasNext()
if (!hasNext) { if (!hasNext) {
if (!snackBar.isShown) { if (!snackBar.isShown) {
snackBar.setText("没有下一页") snackBar.setText("没有下一页")
@ -317,24 +350,12 @@ abstract class PageDelegate(protected val pageView: PageView) :
return hasNext return hasNext
} }
open fun onDestroy() {
bitmap?.recycle()
}
enum class Direction { enum class Direction {
NONE, PREV, NEXT 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 package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.graphics.Matrix
import android.view.MotionEvent import android.view.MotionEvent
import android.view.VelocityTracker
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import kotlin.math.abs import kotlin.math.abs
class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
private val bitmapMatrix = Matrix() // 滑动追踪的时间
private val velocityDuration = 1000
//速度追踪器
private val mVelocity: VelocityTracker = VelocityTracker.obtain()
override fun onAnimStart() { override fun onAnimStart() {
if (!atTop && !atBottom) { //惯性滚动
stopScroll() fling(
return 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) { override fun onScroll() {
if (atTop || atBottom) { curPage.onScroll(touchY - lastY)
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 override fun onTouch(event: MotionEvent) {
if (atTop && mDirection == Direction.PREV) { when (event.action) {
bitmap?.let { MotionEvent.ACTION_DOWN -> {
bitmapMatrix.setTranslate(0.toFloat(), distanceY) setStartPoint(event.x, event.y)
canvas.drawBitmap(it, bitmapMatrix, null) abort()
} mVelocity.clear()
} else if (atBottom && mDirection == Direction.NEXT) { }
bitmap?.let { MotionEvent.ACTION_MOVE -> {
bitmapMatrix.setTranslate(0.toFloat(), distanceY) if (isTextSelected) {
canvas.drawBitmap(it, bitmapMatrix, null) selectText(event)
} else {
onScroll(event)
} }
} }
} }
super.onTouch(event)
} }
override fun onAnimStop() { private fun onScroll(event: MotionEvent) {
if (!isCancel) { mVelocity.addMovement(event)
pageView.fillPage(mDirection) mVelocity.computeCurrentVelocity(velocityDuration)
} setTouchPoint(event.x, event.y)
} if (!isMoved) {
isMoved = abs(startX - event.x) > slop || abs(startY - event.y) > slop
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()
}
} else {
if (atBottom) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage.dispatchTouchEvent(event)
event.recycle()
//如果不存在表示没有下一页了
if (!hasNext()) {
noNext = true
return true
}
setDirection(Direction.NEXT)
setBitmap()
}
}
isMoved = true
}
if ((atTop && mDirection != Direction.PREV) || (atBottom && mDirection != Direction.NEXT) || mDirection == Direction.NONE) {
//传递触摸事件到textView
curPage.dispatchTouchEvent(e2)
} }
if (isMoved) { if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) distanceY < 0 else distanceY > 0
isRunning = true 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 mMiddleY = 0f
private var mDegrees = 0f private var mDegrees = 0f
private var mTouchToCornerDis = 0f private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter = ColorMatrixColorFilter( private var mColorMatrixFilter = ColorMatrixColorFilter(
ColorMatrix( ColorMatrix(
floatArrayOf( floatArrayOf(
1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f,
@ -55,7 +55,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
// 是否属于右上左下 // 是否属于右上左下
private var mIsRtOrLb = false private var mIsRtOrLb = false
private var mMaxLength = 0f private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
// 背面颜色组 // 背面颜色组
private var mBackShadowColors: IntArray private var mBackShadowColors: IntArray
// 前面颜色组 // 前面颜色组
@ -71,15 +71,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private var mFrontShadowDrawableVLR: GradientDrawable private var mFrontShadowDrawableVLR: GradientDrawable
private var mFrontShadowDrawableVRL: 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 curBitmap: Bitmap? = null
private var prevBitmap: Bitmap? = null private var prevBitmap: Bitmap? = null
private var nextBitmap: Bitmap? = null private var nextBitmap: Bitmap? = null
init { init {
mMaxLength = hypot(viewWidth.toDouble(), viewWidth.toDouble()).toFloat()
mPaint.style = Paint.Style.FILL
//设置颜色数组 //设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd) val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color) mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color)
@ -115,13 +113,18 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT 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) { override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
super.setStartPoint(x, y, invalidate) super.setStartPoint(x, y, invalidate)
calcCornerXY(x, y) calcCornerXY(x, y)
} }
override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) { override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
super.setTouchPoint(x, y, invalidate) super.setTouchPoint(x, y, false)
//触摸y中间位置吧y变成屏幕高度 //触摸y中间位置吧y变成屏幕高度
if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66) if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66)
|| mDirection == Direction.PREV || mDirection == Direction.PREV
@ -134,6 +137,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
) { ) {
touchY = 1f touchY = 1f
} }
pageView.invalidate()
} }
override fun setDirection(direction: Direction) { override fun setDirection(direction: Direction) {
@ -171,7 +175,6 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
override fun onAnimStart() { override fun onAnimStart() {
var dx: Float var dx: Float
val dy: Float val dy: Float
// dx 水平方向滑动的距离,负值会使滚动向左滚动
// dy 垂直方向滑动的距离,负值会使滚动向上滚动 // dy 垂直方向滑动的距离,负值会使滚动向上滚动
if (isCancel) { if (isCancel) {
dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { dx = if (mCornerX > 0 && mDirection == Direction.NEXT) {
@ -215,18 +218,22 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (mDirection === Direction.NEXT) { when (mDirection) {
calcPoints() Direction.NEXT -> {
drawCurrentPageArea(canvas, curBitmap, mPath0) calcPoints()
drawNextPageAreaAndShadow(canvas, nextBitmap) drawCurrentPageArea(canvas, curBitmap)
drawCurrentPageShadow(canvas) drawNextPageAreaAndShadow(canvas, nextBitmap)
drawCurrentBackArea(canvas, curBitmap) drawCurrentPageShadow(canvas)
} else { drawCurrentBackArea(canvas, curBitmap)
calcPoints() }
drawCurrentPageArea(canvas, prevBitmap, mPath0) Direction.PREV -> {
drawNextPageAreaAndShadow(canvas, curBitmap) calcPoints()
drawCurrentPageShadow(canvas) drawCurrentPageArea(canvas, prevBitmap)
drawCurrentBackArea(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 left: Int
val right: Int val right: Int
if (mIsRtOrLb) { if (mIsRtOrLb) {
left = mBezierStart1.x.toInt() - 1 left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt() right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR mFolderShadowDrawable = mFolderShadowDrawableLR
} else { } else {
@ -333,17 +340,16 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
var mCurrentPageShadow: GradientDrawable var mCurrentPageShadow: GradientDrawable
if (mIsRtOrLb) { if (mIsRtOrLb) {
leftX = mBezierControl1.x.toInt() leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25 rightX = (mBezierControl1.x + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableVLR mCurrentPageShadow = mFrontShadowDrawableVLR
} else { } else {
leftX = mBezierControl1.x.toInt() - 25 leftX = (mBezierControl1.x - 25).toInt()
rightX = mBezierControl1.x.toInt() + 1 rightX = (mBezierControl1.x + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableVRL mCurrentPageShadow = mFrontShadowDrawableVRL
} }
var rotateDegrees: Float = var rotateDegrees = Math.toDegrees(
Math.toDegrees( atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble()
atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble() ).toFloat()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
leftX, (mBezierControl1.y - mMaxLength).toInt(), leftX, (mBezierControl1.y - mMaxLength).toInt(),
@ -368,24 +374,25 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
if (mIsRtOrLb) { if (mIsRtOrLb) {
leftX = mBezierControl2.y.toInt() leftX = mBezierControl2.y.toInt()
rightX = mBezierControl2.y.toInt() + 25 rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB mCurrentPageShadow = mFrontShadowDrawableHTB
} else { } else {
leftX = mBezierControl2.y.toInt() - 25 leftX = (mBezierControl2.y - 25).toInt()
rightX = mBezierControl2.y.toInt() + 1 rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT mCurrentPageShadow = mFrontShadowDrawableHBT
} }
rotateDegrees = Math.toDegrees( rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble() atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble()
).toFloat() ).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float = val temp =
if (mBezierControl2.y < 0) mBezierControl2.y - viewHeight else mBezierControl2.y if (mBezierControl2.y < 0) (mBezierControl2.y - viewHeight).toDouble()
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() else mBezierControl2.y.toDouble()
val hmg = hypot(mBezierControl2.x.toDouble(), temp)
if (hmg > mMaxLength) if (hmg > mMaxLength)
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX, (mBezierControl2.x - 25 - hmg).toInt(), leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg, rightX (mBezierControl2.x + mMaxLength - hmg).toInt(), rightX
) )
else else
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
@ -396,6 +403,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore() canvas.restore()
} }
//
private fun drawNextPageAreaAndShadow( private fun drawNextPageAreaAndShadow(
canvas: Canvas, canvas: Canvas,
bitmap: Bitmap? bitmap: Bitmap?
@ -443,10 +451,10 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore() canvas.restore()
} }
//
private fun drawCurrentPageArea( private fun drawCurrentPageArea(
canvas: Canvas, canvas: Canvas,
bitmap: Bitmap?, bitmap: Bitmap?
path: Path
) { ) {
bitmap ?: return bitmap ?: return
mPath0.reset() mPath0.reset()
@ -457,11 +465,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close() mPath0.close()
canvas.save() canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path) canvas.clipOutPath(mPath0)
} else { } else {
canvas.clipPath(path, Region.Op.XOR) canvas.clipPath(mPath0, Region.Op.XOR)
} }
canvas.drawBitmap(bitmap, 0f, 0f, null) canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.restore() canvas.restore()
@ -480,8 +489,10 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private fun calcPoints() { private fun calcPoints() {
mTouchX = touchX mTouchX = touchX
mTouchY = touchY mTouchY = touchY
mMiddleX = (mTouchX + mCornerX) / 2 mMiddleX = (mTouchX + mCornerX) / 2
mMiddleY = (mTouchY + mCornerY) / 2 mMiddleY = (mTouchY + mCornerY) / 2
mBezierControl1.x = mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat() mBezierControl1.y = mCornerY.toFloat()
@ -492,6 +503,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} else { } else {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
} }
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
mBezierStart1.y = mCornerY.toFloat() mBezierStart1.y = mCornerY.toFloat()
//固定左边上下两个点 //固定左边上下两个点
@ -500,24 +512,27 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
if (mBezierStart1.x < 0) if (mBezierStart1.x < 0)
mBezierStart1.x = viewWidth - mBezierStart1.x mBezierStart1.x = viewWidth - mBezierStart1.x
val f1: Float = abs(mCornerX - mTouchX) val f1 = abs(mCornerX - mTouchX)
val f2: Float = viewWidth * f1 / mBezierStart1.x val f2 = viewWidth * f1 / mBezierStart1.x
mTouchX = abs(mCornerX - f2) 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) mTouchY = abs(mCornerY - f3)
mMiddleX = (mTouchX + mCornerX) / 2 mMiddleX = (mTouchX + mCornerX) / 2
mMiddleY = (mTouchY + mCornerY) / 2 mMiddleY = (mTouchY + mCornerY) / 2
mBezierControl1.x = mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat() mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat() mBezierControl2.x = mCornerX.toFloat()
mBezierControl2.y = if ((mCornerY - mMiddleY).toInt() == 0) { mBezierControl2.y = if ((mCornerY - mMiddleY).toInt() == 0) {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else { } else {
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) 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() 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( data class TextChar(
val charData: String, val charData: String,
var selected: Boolean = false, var start: Float,
val leftBottomPosition: TextPoint, var end: Float,
val rightTopPosition: TextPoint var selected: Boolean = false
) )

@ -3,8 +3,26 @@ package io.legado.app.ui.book.read.page.entities
data class TextLine( data class TextLine(
var text: String = "", var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(), val textChars: ArrayList<TextChar> = arrayListOf(),
var lineTop: Int = 0, var lineTop: Float = 0f,
var lineBottom: Int = 0, var lineBase: Float = 0f,
var lineBottom: Float = 0f,
val isTitle: Boolean = false, val isTitle: Boolean = false,
var isReadAloud: 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 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.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.read.page.ChapterProvider
data class TextPage( data class TextPage(
var index: Int = 0, var index: Int = 0,
@ -10,9 +13,44 @@ data class TextPage(
val textLines: ArrayList<TextLine> = arrayListOf(), val textLines: ArrayList<TextLine> = arrayListOf(),
var pageSize: Int = 0, var pageSize: Int = 0,
var chapterSize: 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 { fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine -> textLines.forEach { textLine ->
textLine.isReadAloud = false 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.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import kotlinx.android.synthetic.main.item_text.view.* import kotlinx.android.synthetic.main.item_fillet_text.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
class BookAdapter(context: Context, val callBack: CallBack) : 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>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) { 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 { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return true
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
@ -28,7 +24,13 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) { if (oldItem.author != newItem.author) {
return false 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 return false
} }
if (oldItem.coverUrl != newItem.coverUrl) { if (oldItem.coverUrl != newItem.coverUrl) {
@ -56,20 +58,20 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) { if (oldItem.author != newItem.author) {
payload.putString("author", newItem.author) payload.putString("author", newItem.author)
} }
if (oldItem.origins?.size != newItem.origins?.size) { if (oldItem.origins.size != newItem.origins.size) {
payload.putInt("origins", newItem.origins?.size ?: 1) payload.putInt("origins", newItem.origins.size)
} }
if (oldItem.coverUrl != newItem.coverUrl) { if (oldItem.coverUrl != newItem.coverUrl) {
payload.putString("group", newItem.coverUrl) payload.putString("cover", newItem.coverUrl)
} }
if (oldItem.kind != newItem.kind) { if (oldItem.kind != newItem.kind) {
payload.putString("enabled", newItem.kind) payload.putString("kind", newItem.kind)
} }
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
payload.putString("enabled", newItem.latestChapterTitle) payload.putString("last", newItem.latestChapterTitle)
} }
if (oldItem.intro != newItem.intro) { if (oldItem.intro != newItem.intro) {
payload.putString("enabled", newItem.intro) payload.putString("intro", newItem.intro)
} }
return payload return payload
} }

@ -6,7 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.ui.widget.anima.explosion_field.ExplosionField 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.Dispatchers.IO
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,7 +15,7 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) : 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) private val explosionField = ExplosionField.attach2Window(activity)

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

@ -85,8 +85,8 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
private fun precisionSearch(searchBooks: List<SearchBook>) { private fun precisionSearch(searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>() val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook -> searchBooks.forEach { searchBook ->
if (searchBook.name.equals(searchKey, true) if (searchBook.name.contains(searchKey, true)
|| searchBook.author.equals(searchKey, true) || searchBook.author.contains(searchKey, true)
) books.add(searchBook) ) books.add(searchBook)
} }
App.db.searchBookDao().insert(*books.toTypedArray()) App.db.searchBookDao().insert(*books.toTypedArray())
@ -130,9 +130,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} else if (searchKey == item.author) { } else if (searchKey == item.author) {
for ((i, searchBook) in copyDataS.withIndex()) { for ((index, searchBook) in copyDataS.withIndex()) {
if (searchKey != searchBook.name && searchKey == searchBook.author) { if (searchKey != searchBook.name && searchKey == searchBook.author) {
copyDataS.add(i, item) copyDataS.add(index, item)
break 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 searchBooks = copyDataS
searchBookLiveData.postValue(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.lib.theme.ATH
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop 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 kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.share
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import kotlin.math.abs import kotlin.math.abs
@ -84,9 +88,7 @@ class BookSourceEditActivity :
} }
} }
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getSource())?.let { sourceStr -> R.id.menu_share_str -> GSON.toJson(getSource())?.let { share(it) }
shareText(getString(R.string.share_book_source), sourceStr)
}
R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr -> R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_book_source), sourceStr) shareWithQr(getString(R.string.share_book_source), sourceStr)
} }
@ -353,18 +355,16 @@ class BookSourceEditActivity :
} }
private fun showKeyboardTopPopupWindow() { private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return } mSoftKeyboardTool?.let {
if (!isFinishing) { if (it.isShowing) return
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) if (!isFinishing) {
it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
} }
} }
private fun closePopupWindow() { private fun closePopupWindow() {
mSoftKeyboardTool?.let { mSoftKeyboardTool?.dismiss()
if (it.isShowing) {
it.dismiss()
}
}
} }
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { 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.filechooser.FileChooserDialog
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.widget.SelectActionBar 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.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_source.* import kotlinx.android.synthetic.main.activity_book_source.*
@ -109,7 +110,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = BookSourceAdapter(this, this) adapter = BookSourceAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback() 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.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.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_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.* import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()

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

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

@ -14,7 +14,7 @@ import io.legado.app.R
import io.legado.app.base.VMBaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.lib.theme.ATH 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 io.legado.app.utils.getViewModelOfActivity
import kotlinx.android.synthetic.main.fragment_bookmark.* import kotlinx.android.synthetic.main.fragment_bookmark.*
@ -38,7 +38,7 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
adapter = BookmarkAdapter(this) adapter = BookmarkAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter 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.help.BookHelp
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager 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.getViewModelOfActivity
import io.legado.app.utils.observeEvent import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_chapter_list.* import kotlinx.android.synthetic.main.fragment_chapter_list.*
@ -48,7 +48,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
adapter = ChapterListAdapter(requireContext(), this) adapter = ChapterListAdapter(requireContext(), this)
mLayoutManager = UpLinearLayoutManager(requireContext()) mLayoutManager = UpLinearLayoutManager(requireContext())
recycler_view.layoutManager = mLayoutManager recycler_view.layoutManager = mLayoutManager
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }

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

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

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

@ -15,7 +15,11 @@ import io.legado.app.R
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.ui.filechooser.adapter.FileAdapter import io.legado.app.ui.filechooser.adapter.FileAdapter
import io.legado.app.ui.filechooser.adapter.PathAdapter 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.* import kotlinx.android.synthetic.main.dialog_file_chooser.*
@ -133,7 +137,7 @@ class FileChooserDialog : DialogFragment(),
fileAdapter = FileAdapter(requireContext(), this) fileAdapter = FileAdapter(requireContext(), this)
pathAdapter = PathAdapter(requireContext(), this) pathAdapter = PathAdapter(requireContext(), this)
rv_file.addItemDecoration(rv_file.getVerticalDivider()) rv_file.addItemDecoration(VerticalDivider(requireContext()))
rv_file.layoutManager = LinearLayoutManager(activity) rv_file.layoutManager = LinearLayoutManager(activity)
rv_file.adapter = fileAdapter rv_file.adapter = fileAdapter

@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View 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.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup 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.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.arrange.ArrangeBookActivity 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.importbook.ImportBookActivity
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.fragment_bookshelf.* import kotlinx.android.synthetic.main.fragment_bookshelf.*
import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.android.synthetic.main.view_tab_layout.*
@ -61,7 +66,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
super.onCompatOptionsItemSelected(item) super.onCompatOptionsItemSelected(item)
when (item.itemId) { when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>() R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_bookshelf_layout -> selectBookshelfLayout() R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog") .show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>() R.id.menu_add_local -> startActivity<ImportBookActivity>()
@ -145,14 +150,35 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
} }
private fun selectBookshelfLayout() { @SuppressLint("InflateParams")
selector( private fun configBookshelf() {
title = "选择书架布局", requireContext().alert(titleResource = R.string.bookshelf_layout) {
items = resources.getStringArray(R.array.bookshelf_layout).toList() val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
) { _, index -> val bookshelfSort = getPrefInt(PreferKey.bookshelfSort)
putPrefInt(PreferKey.bookshelfLayout, index) val root = LayoutInflater.from(requireContext())
activity?.recreate() .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") @SuppressLint("InflateParams")

@ -103,10 +103,16 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
-3 -> App.db.bookDao().observeAudio() -3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId) 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 val diffResult = DiffUtil
.calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), it)) .calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), books))
booksAdapter.setItems(it, diffResult) 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.ACache
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.item_find_book.view.* import kotlinx.android.synthetic.main.item_find_book.view.*
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
@ -46,7 +46,7 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
gl_child.removeAllViews() gl_child.removeAllViews()
kindList.map { kind -> kindList.map { kind ->
val tv = LayoutInflater.from(context) 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) gl_child.addView(tv)
tv.text_view.text = kind.title tv.text_view.text = kind.title
tv.text_view.onClick { tv.text_view.onClick {

@ -2,6 +2,7 @@ package io.legado.app.ui.qrcode
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -11,7 +12,7 @@ import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat 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.activity_qrcode_capture.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -98,9 +99,11 @@ class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeVie
zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别 zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别
if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) { if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) {
val picturePath = RealPathUtil.getPath(this, it)
// 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调 // 本来就用到 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.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.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_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.* import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, Observer { App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>() 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.filechooser.FileChooserDialog
import io.legado.app.ui.replacerule.edit.ReplaceEditDialog import io.legado.app.ui.replacerule.edit.ReplaceEditDialog
import io.legado.app.ui.widget.SelectActionBar 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.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_replace_rule.* import kotlinx.android.synthetic.main.activity_replace_rule.*
@ -54,6 +55,7 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
private var dataInit = false private var dataInit = false
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initSelectActionView() initSelectActionView()
@ -72,12 +74,28 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
return super.onPrepareOptionsMenu(menu) 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() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
adapter = ReplaceRuleAdapter(this, this) adapter = ReplaceRuleAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(this))
val itemTouchCallback = ItemTouchCallback() val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true itemTouchCallback.isCanDrag = true
@ -306,9 +324,7 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
} }
override fun edit(rule: ReplaceRule) { override fun edit(rule: ReplaceRule) {
ReplaceEditDialog ReplaceEditDialog.show(supportFragmentManager, rule.id)
.newInstance(rule.id)
.show(supportFragmentManager, "editReplace")
} }
override fun toTop(rule: ReplaceRule) { override fun toTop(rule: ReplaceRule) {

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

@ -19,6 +19,12 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application
App.db.replaceRuleDao().findById(id)?.let { App.db.replaceRuleDao().findById(id)?.let {
replaceRuleData.postValue(it) 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.read.ReadRssActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.recycler.LoadMoreView 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 io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_rss_artivles.* import kotlinx.android.synthetic.main.activity_rss_artivles.*
import kotlinx.android.synthetic.main.view_load_more.view.* import kotlinx.android.synthetic.main.view_load_more.view.*
@ -70,7 +70,7 @@ class RssArticlesActivity : VMBaseActivity<RssArticlesViewModel>(R.layout.activi
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = RssArticlesAdapter(this, this) adapter = RssArticlesAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
loadMoreView = LoadMoreView(this) loadMoreView = LoadMoreView(this)

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

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

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.Gravity 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.data.entities.RssSource
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH 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.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop 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 kotlinx.android.synthetic.main.activity_rss_source_edit.*
import org.jetbrains.anko.displayMetrics import org.jetbrains.anko.*
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs import kotlin.math.abs
class RssSourceEditActivity : class RssSourceEditActivity :
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false), VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false),
ViewTreeObserver.OnGlobalLayoutListener,
KeyboardToolPop.CallBack { KeyboardToolPop.CallBack {
private var mSoftKeyboardTool: PopupWindow? = null private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false private var mIsSoftKeyBoardShowing = false
private val qrRequestCode = 101
private val adapter = RssSourceEditAdapter() private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList() private val sourceEntities: ArrayList<EditEntity> = ArrayList()
@ -99,9 +103,10 @@ class RssSourceEditActivity :
clipboard?.setPrimaryClip(ClipData.newPlainText(null, sourceStr)) 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_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getRssSource())?.let { sourceStr -> 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 -> R.id.menu_share_qr -> GSON.toJson(getRssSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_rss_source), sourceStr) shareWithQr(getString(R.string.share_rss_source), sourceStr)
@ -113,7 +118,7 @@ class RssSourceEditActivity :
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
@ -192,37 +197,46 @@ class RssSourceEditActivity :
} }
private fun showKeyboardTopPopupWindow() { private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return } mSoftKeyboardTool?.let {
if (!isFinishing) { if (it.isShowing) return
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) if (!isFinishing) {
it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
} }
} }
private fun closePopupWindow() { private fun closePopupWindow() {
mSoftKeyboardTool?.let { mSoftKeyboardTool?.dismiss()
if (it.isShowing) { }
it.dismiss()
override fun onGlobalLayout() {
val rect = Rect()
// 获取当前页面窗口的显示范围
window.decorView.getWindowVisibleDisplayFrame(rect)
val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val preShowing = mIsSoftKeyBoardShowing
if (abs(keyboardHeight) > screenHeight / 5) {
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
recycler_view.setPadding(0, 0, 0, 100)
showKeyboardTopPopupWindow()
} else {
mIsSoftKeyBoardShowing = false
recycler_view.setPadding(0, 0, 0, 0)
if (preShowing) {
closePopupWindow()
} }
} }
} }
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
override fun onGlobalLayout() { super.onActivityResult(requestCode, resultCode, data)
val rect = Rect() when (requestCode) {
// 获取当前页面窗口的显示范围 qrRequestCode -> if (resultCode == RESULT_OK) {
window.decorView.getWindowVisibleDisplayFrame(rect) data?.getStringExtra("result")?.let {
val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels viewModel.importSource(it) { source: RssSource ->
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 upRecyclerView(source)
val preShowing = mIsSoftKeyBoardShowing }
if (abs(keyboardHeight) > screenHeight / 5) {
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
recycler_view.setPadding(0, 0, 0, 100)
showKeyboardTopPopupWindow()
} else {
mIsSoftKeyBoardShowing = false
recycler_view.setPadding(0, 0, 0, 0)
if (preShowing) {
closePopupWindow()
} }
} }
} }

@ -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.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.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_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.* import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -60,7 +64,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
val groups = linkedSetOf<String>() 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.qrcode.QrCodeActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.SelectActionBar 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.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_source.* 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) get() = getViewModel(RssSourceViewModel::class.java)
private val importRecordKey = "rssSourceRecordKey" private val importRecordKey = "rssSourceRecordKey"
private val qrRequestCode = 101 private val qrRequestCode = 101
private val importSource = 13141 private val importSource = 124
private lateinit var adapter: RssSourceAdapter private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>() private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initLiveDataGroup() initLiveDataGroup()
@ -101,10 +103,26 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
return true 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() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = RssSourceAdapter(this, this) adapter = RssSourceAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback() 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) seek_bar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0)
typedArray.recycle() typedArray.recycle()
iv_seek_add.onClick { iv_seek_plus.onClick {
seek_bar.progressAdd(1) seek_bar.progressAdd(1)
onChanged?.invoke(seek_bar.progress) onChanged?.invoke(seek_bar.progress)
} }
iv_seek_remove.onClick { iv_seek_reduce.onClick {
seek_bar.progressAdd(-1) seek_bar.progressAdd(-1)
onChanged?.invoke(seek_bar.progress) onChanged?.invoke(seek_bar.progress)
} }

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

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

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

Loading…
Cancel
Save