Merge branch 'master' of github.com:gedoor/legado

pull/121/head
fisher 5 years ago
commit 7e9ab18bf8
  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. 31
      app/src/main/assets/updateLog.md
  6. 9
      app/src/main/java/io/legado/app/App.kt
  7. 9
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  8. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  9. 12
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  10. 2
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  11. 8
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  12. 1
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  13. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  14. 12
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  15. 2
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  16. 6
      app/src/main/java/io/legado/app/help/AppConfig.kt
  17. 12
      app/src/main/java/io/legado/app/help/BookHelp.kt
  18. 34
      app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt
  19. 2
      app/src/main/java/io/legado/app/help/ItemTouchCallback.kt
  20. 154
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  21. 10
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  22. 13
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  23. 9
      app/src/main/java/io/legado/app/model/WebBook.kt
  24. 12
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  25. 22
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  26. 21
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  27. 2
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  28. 6
      app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt
  29. 28
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  30. 17
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  31. 12
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  32. 23
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  33. 27
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  34. 21
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  35. 14
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  36. 4
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  37. 4
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  38. 9
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  39. 71
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  40. 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  41. 7
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  42. 95
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  43. 11
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  44. 14
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  45. 41
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  46. 259
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  47. 238
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  48. 563
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  49. 70
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  50. 15
      app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt
  51. 10
      app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt
  52. 118
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  53. 98
      app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt
  54. 37
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  55. 158
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  56. 71
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  57. 81
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  58. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt
  59. 20
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  60. 37
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  61. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPoint.kt
  62. 24
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  63. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  64. 33
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  65. 10
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  66. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  67. 8
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  68. 4
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt
  69. 2
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  70. 4
      app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt
  71. 4
      app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt
  72. 46
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  73. 1
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  74. 4
      app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt
  75. 8
      app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt
  76. 9
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
  77. 8
      app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt
  78. 20
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  79. 4
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt
  80. 4
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt
  81. 23
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  82. 32
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  83. 12
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  84. 8
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  85. 22
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  86. 4
      app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt
  87. 2
      app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt
  88. 8
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  89. 4
      app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt
  90. 47
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  91. 34
      app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt
  92. 8
      app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt
  93. 169
      app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt
  94. 9
      app/src/main/java/io/legado/app/ui/widget/recycler/VerticalDivider.kt
  95. 9
      app/src/main/java/io/legado/app/utils/BitmapUtils.kt
  96. 7
      app/src/main/java/io/legado/app/utils/ColorUtils.kt
  97. 51
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  98. 5
      app/src/main/java/io/legado/app/utils/FloatExtensions.kt
  99. 14
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  100. 2
      app/src/main/res/drawable/recyclerview_divider_horizontal.xml
  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" />

@ -2,6 +2,37 @@
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(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/22**
* 长按选择完成 * 长按选择完成

@ -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() }
}
} }

@ -37,4 +37,6 @@ object PreferKey {
const val lastBackup = "lastBackup" const val lastBackup = "lastBackup"
const val bodyIndent = "textIndent" const val bodyIndent = "textIndent"
const val shareLayout = "shareLayout" 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,
@ -268,7 +274,9 @@ object BookHelp {
} }
} }
} }
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)
2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c) 2 -> c = ZhConvertBootstrap.newInstance().toTraditional(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,17 +111,118 @@ 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,//段距
@ -138,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) {
@ -181,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,7 +86,8 @@ 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 =
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml book.infoHtml
} else { } else {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
@ -112,7 +113,8 @@ 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 =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml book.tocHtml
} else { } else {
AnalyzeUrl( AnalyzeUrl(
@ -141,7 +143,8 @@ 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 =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml book.tocHtml
} else { } else {
val analyzeUrl = val analyzeUrl =

@ -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()) {
return getDefaultRules()
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let { GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray()) App.db.txtTocRule().insert(*it.toTypedArray())
return it return it
} }
} }
} return emptyList()
return rules
} }
} }

@ -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,10 +33,14 @@ 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(
analyzeRule.setContent(body, baseUrl),
contentRule, bookChapter, bookSource
)
content.append(contentData.content.replace(bookChapter.title, "")) 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]
@ -58,8 +62,8 @@ object BookContent {
.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 ""
@ -83,8 +87,8 @@ object BookContent {
.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
} }
@ -94,6 +98,7 @@ object BookContent {
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()

@ -26,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
@ -127,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)
@ -306,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)
}
}
} }

@ -11,6 +11,7 @@ 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.sendToClip
import io.legado.app.utils.toast import io.legado.app.utils.toast
@ -41,14 +42,14 @@ 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" -> requireContext().sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
} }
@ -56,17 +57,17 @@ class AboutFragment : PreferenceFragmentCompat() {
} }
@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")
} }
} }

@ -9,8 +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.openUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
import org.jetbrains.anko.longToast import org.jetbrains.anko.longToast
import java.net.URLEncoder import java.net.URLEncoder
@ -32,10 +32,10 @@ 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" -> requireContext().sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
@ -43,17 +43,6 @@ class DonateFragment : PreferenceFragmentCompat() {
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 getZfbHb(context: Context) { private fun getZfbHb(context: Context) {
requireContext().sendToClip("537954522") requireContext().sendToClip("537954522")
context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包") context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包")

@ -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,7 +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 androidx.appcompat.view.menu.MenuItemImpl 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
@ -25,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
@ -33,9 +33,9 @@ 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.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.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
@ -62,6 +62,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
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
@ -78,8 +79,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
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 val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -103,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()
} }
@ -214,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)
} }
@ -373,14 +378,17 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
textActionMenu ?: let { textActionMenu ?: let {
textActionMenu = TextActionMenu(this, this) 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 -> textActionMenu?.let { popup ->
if (!popup.isShowing) { if (!popup.isShowing) {
popup.showAtLocation( popup.showAtLocation(cursor_left, Gravity.BOTTOM or Gravity.START, x, y)
cursor_left, } else {
Gravity.BOTTOM or Gravity.START, popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT)
cursor_left.x.toInt() + cursor_left.width,
page_view.height - cursor_left.y.toInt() + ReadBookConfig.durConfig.textSize.dp + popup.height
)
} }
} }
} }
@ -393,10 +401,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/** /**
* 文本选择菜单操作 * 文本选择菜单操作
*/ */
override fun onMenuItemSelected(item: MenuItemImpl): Boolean { override fun onMenuItemSelected(itemId: Int): Boolean {
when (item.itemId) { when (itemId) {
R.id.menu_replace -> ReplaceEditDialog R.id.menu_replace -> {
.show(supportFragmentManager, pattern = selectedText) ReplaceEditDialog.show(supportFragmentManager, pattern = selectedText)
return true
}
} }
return false return false
} }
@ -406,6 +416,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/ */
override fun onMenuActionFinally() { override fun onMenuActionFinally() {
textActionMenu?.dismiss() textActionMenu?.dismiss()
page_view.curPage.cancelSelect()
page_view.pageDelegate?.isTextSelected = false page_view.pageDelegate?.isTextSelected = false
} }
@ -443,9 +454,9 @@ 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)
} }
} }
@ -492,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()
@ -587,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) {
@ -621,7 +633,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
super.onDestroy() super.onDestroy()
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss() textActionMenu?.dismiss()
page_view.pageDelegate?.onDestroy() page_view.onDestroy()
} }
override fun observeLiveBus() { override fun observeLiveBus() {
@ -643,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 {
@ -680,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)
} }
} }
@ -699,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
} }
} }

@ -1,24 +1,37 @@
package io.legado.app.ui.book.read package io.legado.app.ui.book.read
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.Context 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.LayoutInflater
import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip 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.item_fillet_text.view.*
import kotlinx.android.synthetic.main.popup_action_menu.view.* import kotlinx.android.synthetic.main.popup_action_menu.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.share
import org.jetbrains.anko.toast
@SuppressLint("RestrictedApi")
class TextActionMenu(private val context: Context, private val callBack: CallBack) : class TextActionMenu(private val context: Context, private val callBack: CallBack) :
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
@ -40,6 +53,26 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
val menu = MenuBuilder(context) val menu = MenuBuilder(context)
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems) adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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) : inner class Adapter(context: Context) :
@ -58,7 +91,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
override fun registerListener(holder: ItemViewHolder) { override fun registerListener(holder: ItemViewHolder) {
holder.itemView.onClick { holder.itemView.onClick {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
if (!callBack.onMenuItemSelected(it)) { if (!callBack.onMenuItemSelected(it.itemId)) {
onMenuItemSelected(it) onMenuItemSelected(it)
} }
} }
@ -69,15 +102,71 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
private fun onMenuItemSelected(item: MenuItemImpl) { private fun onMenuItemSelected(item: MenuItemImpl) {
when (item.itemId) { when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText) R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(callBack.selectedText)
R.id.menu_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() 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 { interface CallBack {
val selectedText: String val selectedText: String
fun onMenuItemSelected(item: MenuItemImpl): Boolean fun onMenuItemSelected(itemId: Int): Boolean
fun onMenuActionFinally() fun onMenuActionFinally()
} }

@ -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,9 @@ 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.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
@ -76,8 +78,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
private fun initData() { private fun initData() {
cb_share_layout.isChecked = getPrefBoolean(PreferKey.shareLayout) cb_share_layout.isChecked = ReadBookConfig.shareLayout
requireContext().getPrefInt(PreferKey.pageAnim).let { 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)
} }
@ -92,14 +94,14 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
tv_title_center.onClick { tv_title_center.onClick {
ReadBookConfig.durConfig.apply { ReadBookConfig.apply {
titleCenter = !titleCenter titleCenter = !titleCenter
tv_title_center.isSelected = titleCenter tv_title_center.isSelected = titleCenter
} }
postEvent(EventBus.UP_CONFIG, true) 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
} }
@ -113,7 +115,7 @@ 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)
} }
} }
@ -125,28 +127,28 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
} }
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) { for (i in 0 until rg_page_anim.childCount) {
if (checkedId == rg_page_anim[i].id) { if (checkedId == rg_page_anim[i].id) {
requireContext().putPrefInt(PreferKey.pageAnim, i) ReadBookConfig.pageAnim = i
val activity = activity val activity = activity
if (activity is ReadBookActivity) { if (activity is ReadBookActivity) {
activity.page_view.upPageAnim(i) activity.page_view.upPageAnim()
} }
break break
} }
@ -154,7 +156,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked -> cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
if (checkBox.isPressed) { if (checkBox.isPressed) {
putPrefBoolean(PreferKey.shareLayout, isChecked) ReadBookConfig.shareLayout = isChecked
upStyle()
postEvent(EventBus.UP_CONFIG, true)
} }
} }
bg0.onClick { changeBg(0) } bg0.onClick { changeBg(0) }
@ -190,7 +194,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
private fun upStyle() { private fun upStyle() {
ReadBookConfig.durConfig.let { ReadBookConfig.let {
tv_title_center.isSelected = it.titleCenter 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
@ -214,12 +218,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,7 +37,7 @@ object ChapterProvider {
private var bodyIndent = BookHelp.bodyIndent private var bodyIndent = BookHelp.bodyIndent
init { init {
upStyle(ReadBookConfig.durConfig) upStyle()
} }
/** /**
@ -73,7 +78,7 @@ object ChapterProvider {
durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder) durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder)
} }
} }
textPages.last().height = durY textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString() 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)
@ -116,12 +121,12 @@ 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 {
textPages.last().height = durY
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)
@ -131,50 +136,24 @@ object ChapterProvider {
textPages.add(TextPage()) textPages.add(TextPage())
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} }
textLine.lineBottom = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineTop = (paddingTop + durY - textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat() (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + titlePaint.fontMetrics.descent
val words = 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 = if (ReadBookConfig.durConfig.titleCenter)
(visibleWidth - layout.getLineMax(lineIndex)) / 2
else 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
@ -198,12 +177,12 @@ 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 {
textPages.last().height = durY
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)
@ -213,95 +192,140 @@ object ChapterProvider {
textPages.add(TextPage()) textPages.add(TextPage())
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} }
textLine.lineBottom = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineTop = (paddingTop + durY - textLine.lineTop = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat() (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat()
var words = textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + contentPaint.fontMetrics.descent
val words =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) 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) {
//第一行 //第一行
addCharsToLineFirst(textLine, words, contentPaint, desiredWidth)
} else if (lineIndex == layout.lineCount - 1) {
//最后一行
addCharsToLineLast(textLine, words, stringBuilder, contentPaint, 0f)
} else {
//中间行
addCharsToLineMiddle(textLine, words, contentPaint, desiredWidth, 0f)
}
}
durY += paragraphSpacing
return durY
}
/**
* 有缩进,两端对齐
*/
private fun addCharsToLineFirst(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float
) {
var x = 0f var x = 0f
val icw = StaticLayout.getDesiredWidth(bodyIndent, contentPaint) / bodyIndent.length val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
for (i in 0..bodyIndent.lastIndex) { for (i in 0..bodyIndent.lastIndex) {
val x1 = x + icw val x1 = x + icw
val textChar = TextChar( textLine.addTextChar(
charData = bodyIndent[i].toString(), charData = bodyIndent[i].toString(),
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), start = paddingLeft + x,
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) end = paddingLeft + x1
) )
textLine.textChars.add(textChar)
x = x1 x = x1
} }
words = words.replaceFirst(bodyIndent, "") val words1 = words.replaceFirst(bodyIndent, "")
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x)
}
/**
* 无缩进,两端对齐
*/
private fun addCharsToLineMiddle(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float,
startX: Float
) {
val gapCount: Int = words.length - 1 val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount val d = (visibleWidth - desiredWidth) / gapCount
var x = startX
for (i in words.indices) { for (i in words.indices) {
val char = words[i].toString() val char = words[i]
val cw = StaticLayout.getDesiredWidth(char, contentPaint) val cw = StaticLayout.getDesiredWidth(char.toString(), textPaint)
val x1 = if (i != words.lastIndex) x + cw + d else x + cw val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw)
val textChar1 = TextChar( textLine.addTextChar(
charData = char, charData = char.toString(),
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), start = paddingLeft + x,
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) end = paddingLeft + x1
) )
textLine.textChars.add(textChar1)
x = x1 x = x1
} }
} else if (lineIndex == layout.lineCount - 1) { exceed(textLine, words)
//最后一行 }
/**
* 最后一行,自然排列
*/
private fun addCharsToLineLast(
textLine: TextLine,
words: String,
stringBuilder: StringBuilder,
textPaint: TextPaint,
startX: Float
) {
stringBuilder.append("\n") stringBuilder.append("\n")
textLine.text = "$words\n" textLine.text = "$words\n"
var x = 0f var x = startX
for (i in words.indices) { for (i in words.indices) {
val char = words[i].toString() val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint) val cw = StaticLayout.getDesiredWidth(char, textPaint)
val x1 = x + cw val x1 = x + cw
val textChar = TextChar( textLine.addTextChar(
charData = char, charData = char,
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), start = paddingLeft + x,
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) end = paddingLeft + x1
) )
textLine.textChars.add(textChar)
x = x1 x = x1
} }
} else { exceed(textLine, words)
//中间行 }
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount /**
var x = 0f * 超出边界处理
for (i in words.indices) { */
val char = words[i].toString() private fun exceed(textLine: TextLine, words: String) {
val cw = StaticLayout.getDesiredWidth(char, contentPaint) val endX = textLine.textChars.last().end
val x1 = if (i != words.lastIndex) x + cw + d else x + cw if (endX > visibleRight) {
val textChar = TextChar( val cc = (endX - visibleRight) / words.length
charData = char, for (i in 0..words.lastIndex) {
leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), textLine.getTextCharReverseAt(i).let {
rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) val py = cc * (words.length - i)
) it.start = it.start - py
textLine.textChars.add(textChar) it.end = it.end - py
x = x1
} }
} }
} }
durY += paragraphSpacing
return durY
} }
/** /**
* 更新样式 * 更新样式
*/ */
fun upStyle(config: ReadBookConfig.Config) { fun upStyle() {
typeface = try { typeface = try {
val fontPath = App.INSTANCE.getPrefString(PreferKey.readBookFont) val fontPath = App.INSTANCE.getPrefString(PreferKey.readBookFont)
if (!TextUtils.isEmpty(fontPath)) { if (!TextUtils.isEmpty(fontPath)) {
Typeface.createFromFile(fontPath) Typeface.createFromFile(fontPath)
} else { } else {
Typeface.SANS_SERIF when (AppConfig.systemTypefaces) {
1 -> Typeface.SERIF
2 -> Typeface.MONOSPACE
else -> Typeface.SANS_SERIF
}
} }
} catch (e: Exception) { } catch (e: Exception) {
App.INSTANCE.removePref(PreferKey.readBookFont) App.INSTANCE.removePref(PreferKey.readBookFont)
@ -309,34 +333,36 @@ object ChapterProvider {
} }
//标题 //标题
titlePaint.isAntiAlias = true titlePaint.isAntiAlias = true
titlePaint.color = config.textColor() titlePaint.color = ReadBookConfig.durConfig.textColor()
titlePaint.letterSpacing = config.letterSpacing titlePaint.letterSpacing = ReadBookConfig.letterSpacing
titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD) titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD)
//正文 //正文
contentPaint.isAntiAlias = true contentPaint.isAntiAlias = true
contentPaint.color = config.textColor() contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = config.letterSpacing contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (config.textBold) Typeface.BOLD else Typeface.NORMAL val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold) contentPaint.typeface = Typeface.create(typeface, bold)
//间距 //间距
lineSpacingExtra = config.lineSpacingExtra.dp.toFloat() lineSpacingExtra = ReadBookConfig.lineSpacingExtra.dp.toFloat()
paragraphSpacing = config.paragraphSpacing.dp paragraphSpacing = ReadBookConfig.paragraphSpacing.dp
titlePaint.textSize = (config.textSize + 2).dp.toFloat() titlePaint.textSize = (ReadBookConfig.textSize + 2).dp.toFloat()
contentPaint.textSize = config.textSize.dp.toFloat() contentPaint.textSize = ReadBookConfig.textSize.dp.toFloat()
bodyIndent = BookHelp.bodyIndent bodyIndent = BookHelp.bodyIndent
upSize(config) upSize()
} }
/** /**
* 更新View尺寸 * 更新View尺寸
*/ */
fun upSize(config: ReadBookConfig.Config) { fun upSize() {
paddingLeft = config.paddingLeft.dp paddingLeft = ReadBookConfig.paddingLeft.dp
paddingTop = config.paddingTop.dp paddingTop = ReadBookConfig.paddingTop.dp
visibleWidth = viewWidth - paddingLeft - config.paddingRight.dp visibleWidth = viewWidth - paddingLeft - ReadBookConfig.paddingRight.dp
visibleHeight = viewHeight - paddingTop - config.paddingBottom.dp visibleHeight = viewHeight - paddingTop - ReadBookConfig.paddingBottom.dp
visibleRight = paddingLeft + visibleWidth
visibleBottom = paddingTop + visibleHeight
} }
} }

@ -3,15 +3,14 @@ package io.legado.app.ui.book.read.page
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.text.Layout import android.graphics.RectF
import android.text.StaticLayout
import android.util.AttributeSet import android.util.AttributeSet
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.service.help.ReadBook 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.activity
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
@ -19,26 +18,26 @@ 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
} }
} }
private var activityCallBack: CallBack private var callBack: CallBack
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) private val visibleRect = RectF()
private var selectLineStart = 0 private val selectStart = arrayOf(0, 0, 0)
private var selectCharStart = 0 private val selectEnd = arrayOf(0, 0, 0)
private var selectLineEnd = 0
private var selectCharEnd = 0
private var textPage: TextPage = TextPage() private var textPage: TextPage = TextPage()
//滚动参数 //滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f private val maxScrollOffset = 100f
private var pageOffset = 0f private var pageOffset = 0f
private var isLastPage = false
init { init {
activityCallBack = activity as CallBack callBack = activity as CallBack
contentDescription = textPage.text contentDescription = textPage.text
} }
@ -48,72 +47,114 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
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(ReadBookConfig.durConfig) ChapterProvider.upSize()
} upVisibleRect()
textPage.format()
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
if (textPage.textLines.isEmpty()) { canvas.clipRect(visibleRect)
drawMsg(canvas, textPage.text) drawPage(canvas)
} else {
drawHorizontalPage(canvas)
}
} }
@Suppress("DEPRECATION") /**
private fun drawMsg(canvas: Canvas, msg: String) { * 绘制页面
val layout = StaticLayout( */
msg, ChapterProvider.contentPaint, width, private fun drawPage(canvas: Canvas) {
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false 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
) )
val y = (height - layout.height) / 2f
for (lineIndex in 0 until layout.lineCount) {
val x = (width - layout.getLineMax(lineIndex)) / 2
val words =
msg.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
canvas.drawText(words, x, y, ChapterProvider.contentPaint)
} }
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)
private fun drawHorizontalPage(canvas: Canvas) { if (relativeOffset < ChapterProvider.visibleHeight) {
textPage.textLines.forEach { textLine -> relativePage(2).textLines.forEach { textLine ->
val textPaint = if (textLine.isTitle) { val lineTop = textLine.lineTop + relativeOffset
ChapterProvider.titlePaint val lineBase = textLine.lineBase + relativeOffset
} else { val lineBottom = textLine.lineBottom + relativeOffset
ChapterProvider.contentPaint drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
} }
textPaint.color = if (textLine.isReadAloud) {
context.accentColor
} else {
ReadBookConfig.durConfig.textColor()
} }
textLine.textChars.forEach {
canvas.drawText(
it.charData,
it.leftBottomPosition.x,
it.leftBottomPosition.y,
textPaint
)
if (it.selected) {
canvas.drawRect(
it.leftBottomPosition.x,
it.rightTopPosition.y,
it.rightTopPosition.x,
it.leftBottomPosition.y,
selectedPaint
)
} }
/**
* 绘制文字
*/
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) { fun onScroll(mOffset: Float) {
if (mOffset == 0f) return
var offset = mOffset var offset = mOffset
if (offset > maxScrollOffset) { if (offset > maxScrollOffset) {
offset = maxScrollOffset offset = maxScrollOffset
@ -121,102 +162,315 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
offset = -maxScrollOffset offset = -maxScrollOffset
} }
if (!isLastPage || offset < 0) {
pageOffset += offset pageOffset += offset
isLastPage = false if (pageOffset > 0) {
} if (!pageFactory.hasPrev()) {
// 首页
if (pageOffset < 0 && ReadBook.durChapterIndex == 0 && ReadBook.durPageIndex == 0) {
pageOffset = 0f pageOffset = 0f
} else {
pageFactory.moveToPrev()
textPage = pageFactory.currentPage
pageOffset -= textPage.height
upView?.invoke(textPage)
}
} else if (pageOffset < -textPage.height) {
if (!pageFactory.hasNext()) {
pageOffset = -textPage.height.toFloat()
} else {
pageOffset += textPage.height
pageFactory.moveToNext()
textPage = pageFactory.currentPage
upView?.invoke(textPage)
}
}
invalidate()
} }
fun resetPageOffset() {
pageOffset = 0f
} }
fun selectText(x: Float, y: Float): Boolean { /**
* 选择初始文字
*/
fun selectText(
x: Float,
y: Float,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
) {
if (!selectAble) return
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) { if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { if (x > textChar.start && x < textChar.end) {
textChar.selected = true textChar.selected = true
invalidate() invalidate()
selectLineStart = lineIndex selectStart[0] = 0
selectCharStart = charIndex selectStart[1] = lineIndex
selectLineEnd = lineIndex selectStart[2] = charIndex
selectCharEnd = charIndex selectEnd[0] = 0
upSelectedStart( selectEnd[1] = lineIndex
textChar.leftBottomPosition.x, selectEnd[2] = charIndex
textChar.leftBottomPosition.y upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectedEnd( select(0, lineIndex, charIndex)
textChar.rightTopPosition.x, return
textChar.leftBottomPosition.y }
) }
return true return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
val nextPage = relativePage(1)
for ((lineIndex, textLine) in nextPage.textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(1, lineIndex, charIndex)
return
}
} }
return
} }
break }
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(2, lineIndex, charIndex)
return
}
}
return
} }
} }
return false
} }
/**
* 开始选择符移动
*/
fun selectStartMove(x: Float, y: Float) { fun selectStartMove(x: Float, y: Float) {
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) { if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { if (x > textChar.start && x < textChar.end) {
if (selectLineStart != lineIndex || selectCharStart != charIndex) { if (selectStart[0] != 0 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
selectLineStart = lineIndex if (selectToInt(0, lineIndex, charIndex) > selectToInt(selectEnd)) {
selectCharStart = charIndex return
upSelectedStart(
textChar.leftBottomPosition.x,
textChar.leftBottomPosition.y
)
upSelectChars(textPage)
} }
break selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 1 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(1, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 2 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(2, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
} }
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
} }
break return
}
}
return
} }
} }
} }
/**
* 结束选择符移动
*/
fun selectEndMove(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()) { for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) { if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { if (x > textChar.start && x < textChar.end) {
if (selectLineEnd != lineIndex || selectCharEnd != charIndex) { if (selectEnd[0] != 0 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
selectLineEnd = lineIndex if (selectToInt(0, lineIndex, charIndex) < selectToInt(selectStart)) {
selectCharEnd = charIndex return
upSelectedEnd( }
textChar.rightTopPosition.x, selectEnd[0] = 0
textChar.leftBottomPosition.y selectEnd[1] = lineIndex
) selectEnd[2] = charIndex
upSelectChars(textPage) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
} }
break }
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
} }
} }
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 (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
} }
} }
} }
private fun upSelectChars(textPage: TextPage) { /**
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { * 选择开始文字
*/
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectStart[0] = relativePage
selectStart[1] = lineIndex
selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
/**
* 选择结束文字
*/
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectEnd[0] = relativePage
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
private fun upSelectChars() {
val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
textChar.selected = textChar.selected =
if (lineIndex == selectLineStart && lineIndex == selectLineEnd) { if (relativePos == selectStart[0]
charIndex in selectCharStart..selectCharEnd && relativePos == selectEnd[0]
} else if (lineIndex == selectLineStart) { && lineIndex == selectStart[1]
charIndex >= selectCharStart && lineIndex == selectEnd[1]
} else if (lineIndex == selectLineEnd) { ) {
charIndex <= selectCharEnd 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 { } else {
lineIndex in (selectLineStart + 1) until selectLineEnd relativePos in selectStart[0] + 1 until selectEnd[0]
}
} }
} }
} }
@ -224,55 +478,114 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
private fun upSelectedStart(x: Float, y: Float) { private fun upSelectedStart(x: Float, y: Float) {
activityCallBack.upSelectedStart(x, y + activityCallBack.headerHeight) callBack.upSelectedStart(x, y + callBack.headerHeight)
} }
private fun upSelectedEnd(x: Float, y: Float) { private fun upSelectedEnd(x: Float, y: Float) {
activityCallBack.upSelectedEnd(x, y + activityCallBack.headerHeight) callBack.upSelectedEnd(x, y + callBack.headerHeight)
} }
fun cancelSelect() { fun cancelSelect() {
textPage.textLines.forEach { textLine -> val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
relativePage(relativePos).textLines.forEach { textLine ->
textLine.textChars.forEach { textLine.textChars.forEach {
it.selected = false it.selected = false
} }
} }
}
invalidate() invalidate()
activityCallBack.onCancelSelect() callBack.onCancelSelect()
} }
val selectedText: String val selectedText: String
get() { get() {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
for (lineIndex in selectLineStart..selectLineEnd) { for (relativePos in selectStart[0]..selectEnd[0]) {
if (lineIndex == selectLineStart && lineIndex == selectLineEnd) { 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( stringBuilder.append(
textPage.textLines[lineIndex].text.substring( textPage.textLines[lineIndex].text.substring(
selectCharStart, selectStart[2]
selectCharEnd + 1
) )
) )
} else if (lineIndex == selectLineStart) { } 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( stringBuilder.append(
textPage.textLines[lineIndex].text.substring( textPage.textLines[lineIndex].text.substring(
selectCharStart selectStart[2]
) )
) )
} else if (lineIndex == selectLineEnd) { } else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos == selectEnd[0]) {
for (lineIndex in 0..selectEnd[1]) {
if (lineIndex == selectEnd[1]) {
stringBuilder.append( stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectCharEnd + 1) textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1)
) )
} else { } else {
stringBuilder.append(textPage.textLines[lineIndex].text) 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() 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 { interface CallBack {
fun upSelectedStart(x: Float, y: Float) fun upSelectedStart(x: Float, y: Float)
fun upSelectedEnd(x: Float, y: Float) fun upSelectedEnd(x: Float, y: Float)
fun onCancelSelect() fun onCancelSelect()
val headerHeight: Int val headerHeight: Int
val pageFactory: TextPageFactory
} }
} }

@ -5,9 +5,10 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
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.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.* import io.legado.app.utils.*
@ -16,8 +17,6 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) { class ContentView(context: Context) : FrameLayout(context) {
var callBack: CallBack? = null
private var pageSize: Int = 0
init { init {
//设置背景防止切换背景时文字重叠 //设置背景防止切换背景时文字重叠
@ -26,18 +25,23 @@ class ContentView(context: Context) : FrameLayout(context) {
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
//显示状态栏时隐藏header //显示状态栏时隐藏header
if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams = ll_header.layoutParams.apply {
ll_header.layoutParams.apply { height = context.getStatusBarHeight() } height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding( ll_header.setPadding(
headerPaddingLeft.dp, headerPaddingLeft.dp,
headerPaddingTop.dp, headerPaddingTop.dp,
@ -48,21 +52,16 @@ class ContentView(context: Context) : FrameLayout(context) {
page_panel.setPadding(0, 0, 0, 0) page_panel.setPadding(0, 0, 0, 0)
} else { } else {
ll_header.gone() ll_header.gone()
page_panel.setPadding(0, context.getStatusBarHeight(), 0, 0) page_panel.setPadding(0, context.statusBarHeight, 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)
@ -73,10 +72,10 @@ class ContentView(context: Context) : FrameLayout(context) {
val headerHeight: Int val headerHeight: Int
get() { get() {
return if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { return if (ReadBookConfig.hideStatusBar) {
ll_header.height ll_header.height
} else { } else {
context.getStatusBarHeight() context.statusBarHeight
} }
} }
@ -92,17 +91,23 @@ class ContentView(context: Context) : FrameLayout(context) {
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) {
if (textPage != null) { tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage) content_text_view.setContent(textPage)
tv_bottom_left.text = textPage.title
pageSize = textPage.pageSize
setPageIndex(textPage.index)
} }
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}"
} }
@ -116,27 +121,34 @@ class ContentView(context: Context) : FrameLayout(context) {
content_text_view.selectAble = selectAble content_text_view.selectAble = selectAble
} }
fun selectText(e: MotionEvent): Boolean { fun selectText(
e: MotionEvent,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
) {
val y = e.y - headerHeight val y = e.y - headerHeight
return content_text_view.selectText(e.x, y) return content_text_view.selectText(e.x, y, select)
} }
fun selectStartMove(x: Float, y: Float) { fun selectStartMove(x: Float, y: Float) {
content_text_view.selectStartMove(x, y - headerHeight) content_text_view.selectStartMove(x, y - headerHeight)
} }
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
fun selectEndMove(x: Float, y: Float) { fun selectEndMove(x: Float, y: Float) {
content_text_view.selectEndMove(x, y - headerHeight) content_text_view.selectEndMove(x, y - headerHeight)
} }
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
fun cancelSelect() { fun cancelSelect() {
content_text_view.cancelSelect() content_text_view.cancelSelect()
} }
val selectedText: String get() = content_text_view.selectedText val selectedText: String get() = content_text_view.selectedText
interface CallBack {
fun scrollToLine(line: Int)
fun scrollToLast()
}
} }

@ -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,21 +6,18 @@ 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.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
var pageDelegate: PageDelegate? = null var pageDelegate: PageDelegate? = null
var prevPage: ContentView var prevPage: ContentView
@ -28,7 +25,7 @@ class PageView(context: Context, attrs: AttributeSet) :
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)
@ -38,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) {
@ -66,28 +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()
} }
PageDelegate.Direction.NEXT -> { PageDelegate.Direction.NEXT -> {
pageFactory?.moveToNext() pageFactory.moveToNext()
upContent() upContent()
} }
else -> Unit else -> Unit
} }
} }
fun upPageAnim(pageAnim: Int) { fun upPageAnim() {
pageDelegate?.onDestroy() 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)
@ -97,19 +99,21 @@ 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)
} }
} }
} }
callBack?.screenOffTimerStart() callBack.screenOffTimerStart()
} }
fun moveToPrevPage(noAnim: Boolean = true) { fun moveToPrevPage(noAnim: Boolean = true) {
@ -128,12 +132,8 @@ class PageView(context: Context, attrs: AttributeSet) :
} }
} }
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()
@ -160,30 +160,19 @@ class PageView(context: Context, attrs: AttributeSet) :
nextPage.upBattery(battery) nextPage.upBattery(battery)
} }
override val isScrollDelegate: Boolean override val currentChapter: TextChapter?
get() = pageDelegate is ScrollPageDelegate get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
override val pageIndex: Int
get() = ReadBook.durChapterPos()
override fun setPageIndex(pageIndex: Int) {
callBack?.setPageIndex(pageIndex)
}
override fun getChapterPosition(): Int {
return ReadBook.durChapterIndex
}
override fun getCurrentChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null
} }
override fun getNextChapter(): TextChapter? { override val nextChapter: TextChapter?
return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null get() {
return if (callBack.isInitFinish) ReadBook.textChapter(1) else null
} }
override fun getPreviousChapter(): TextChapter? { override val prevChapter: TextChapter?
return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null get() {
return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null
} }
override fun hasNextChapter(): Boolean { override fun hasNextChapter(): Boolean {
@ -191,45 +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()
} }
} }

@ -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 {
}
getCurrentChapter()?.let {
if (pageIndex < it.pageSize() - 1) { if (pageIndex < it.pageSize() - 1) {
return getCurrentChapter()?.page(pageIndex + 1)?.removePageAloudSpan() return@with it.page(pageIndex + 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
} }
} }
return getNextChapter()?.page(0)?.removePageAloudSpan() nextChapter?.let {
return@with it.page(0)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
} }
return TextPage().format()
override fun previousPage(): TextPage? = with(dataSource) {
if (isScrollDelegate) {
return getPreviousChapter()?.scrollPage()
} }
override val prevPage: TextPage
get() = with(dataSource) {
if (pageIndex > 0) { if (pageIndex > 0) {
return getCurrentChapter()?.page(pageIndex - 1)?.removePageAloudSpan() currentChapter?.let {
return@with it.page(pageIndex - 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
} }
return getPreviousChapter()?.lastPage()?.removePageAloudSpan() }
prevChapter?.let {
return@with it.lastPage()?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
return TextPage().format()
} }
override val nextPagePlus: TextPage
get() = with(dataSource) {
currentChapter?.let {
if (pageIndex < it.pageSize() - 2) {
return@with it.page(pageIndex + 2)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
nextChapter?.let { nc ->
if (pageIndex < it.pageSize() - 1) {
return@with nc.page(0)?.removePageAloudSpan()
?: TextPage(title = nc.title).format()
}
return@with nc.page(1)?.removePageAloudSpan()
?: TextPage(title = nc.title).format()
}
}
return TextPage().format()
}
} }

@ -6,19 +6,30 @@ import kotlin.math.abs
abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { 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,6 +24,7 @@ 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 = 0f protected var startX: Float = 0f
protected var startY: Float = 0f protected var startY: Float = 0f
@ -35,35 +35,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
protected var touchX: Float = 0f protected var touchX: Float = 0f
protected var touchY: Float = 0f 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
@ -75,6 +65,15 @@ abstract class PageDelegate(protected val pageView: PageView) :
var isRunning = false var isRunning = false
var isStarted = false var isStarted = false
var isTextSelected = 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
@ -85,7 +84,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
touchY = y touchY = y
if (invalidate) { if (invalidate) {
invalidate() pageView.invalidate()
} }
} }
@ -96,20 +95,12 @@ abstract class PageDelegate(protected val pageView: PageView) :
touchY = y touchY = y
if (invalidate) { if (invalidate) {
invalidate() pageView.invalidate()
} }
onScroll() onScroll()
} }
fun upSelectAble(selectAble: Boolean) {
detector.setIsLongpressEnabled(selectAble)
}
protected fun invalidate() {
pageView.invalidate()
}
open fun fling( open fun fling(
startX: Int, startY: Int, velocityX: Int, velocityY: Int, startX: Int, startY: Int, velocityX: Int, velocityY: Int,
minX: Int, maxX: Int, minY: Int, maxY: Int minX: Int, maxX: Int, minY: Int, maxY: Int
@ -117,7 +108,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)
isRunning = true isRunning = true
isStarted = true isStarted = true
invalidate() pageView.invalidate()
} }
protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) { protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) {
@ -130,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) {
pageView.postDelayed({
bitmap?.recycle()
bitmap = null
}, 100)
} else {
bitmap?.recycle() bitmap?.recycle()
bitmap = null 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
@ -162,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()
} }
@ -202,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) {
@ -228,9 +210,17 @@ 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
return detector.onTouchEvent(event) if (!detector.onTouchEvent(event)) {
//GestureDetector.onFling小幅移动不会触发,所以要自己判断
if (event.action == MotionEvent.ACTION_UP && isMoved) {
if (selectedOnDown) {
selectedOnDown = false
}
if (!noNext) onAnimStart()
}
}
} }
/** /**
@ -239,6 +229,8 @@ abstract class PageDelegate(protected val pageView: PageView) :
override fun onDown(e: MotionEvent): Boolean { override fun onDown(e: MotionEvent): Boolean {
if (isTextSelected) { if (isTextSelected) {
curPage.cancelSelect() curPage.cancelSelect()
isTextSelected = false
selectedOnDown = true
} }
//是否移动 //是否移动
isMoved = false isMoved = false
@ -259,29 +251,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 单击 * 单击
*/ */
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
if (isTextSelected) { if (selectedOnDown) {
isTextSelected = false selectedOnDown = false
return true 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()
} }
@ -295,30 +283,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 长按选择 * 长按选择
*/ */
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent) {
isTextSelected = curPage.selectText(e) curPage.selectText(e) { relativePage, lineIndex, charIndex ->
isTextSelected = true
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 (isTextSelected) { curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
isTextSelected = false }
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)
}
}
} }
if (!noNext) onAnimStart()
return true
} }
/** /**
* 判断是否有上一页 * 判断是否有上一页
*/ */
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("没有上一页")
@ -332,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("没有下一页")

@ -1,6 +1,5 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.view.MotionEvent import android.view.MotionEvent
import android.view.VelocityTracker import android.view.VelocityTracker
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
@ -21,70 +20,38 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
) )
} }
override fun onDraw(canvas: Canvas) { override fun onScroll() {
curPage.onScroll(lastY - touchY) curPage.onScroll(touchY - lastY)
} }
override fun onAnimStop() { override fun onTouch(event: MotionEvent) {
if (!isCancel) { when (event.action) {
pageView.fillPage(mDirection) MotionEvent.ACTION_DOWN -> {
} setStartPoint(event.x, event.y)
}
override fun onDown(e: MotionEvent): Boolean {
abort() abort()
mVelocity.clear() mVelocity.clear()
mVelocity.addMovement(e)
return super.onDown(e)
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
mVelocity.addMovement(e2)
mVelocity.computeCurrentVelocity(velocityDuration)
if (!isMoved && abs(distanceX) < abs(distanceY)) {
if (distanceY < 0) {
if (atTop) {
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
}
setDirection(Direction.PREV)
} }
MotionEvent.ACTION_MOVE -> {
if (isTextSelected) {
selectText(event)
} else { } else {
if (atBottom) { onScroll(event)
//如果不存在表示没有下一页了 }
if (!hasNext()) {
noNext = true
return true
} }
setDirection(Direction.NEXT)
} }
super.onTouch(event)
} }
isMoved = true
private fun onScroll(event: MotionEvent) {
mVelocity.addMovement(event)
mVelocity.computeCurrentVelocity(velocityDuration)
setTouchPoint(event.x, event.y)
if (!isMoved) {
isMoved = abs(startX - event.x) > slop || abs(startY - event.y) > slop
} }
if (isMoved) { if (isMoved) {
isRunning = true isRunning = true
//设置触摸点
setTouchPoint(e2.x, e2.y)
}
return isMoved
} }
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
mVelocity.addMovement(e2)
return super.onFling(e1, e2, velocityX, velocityY)
} }
override fun onDestroy() { override fun onDestroy() {

@ -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,19 +218,23 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (mDirection === Direction.NEXT) { when (mDirection) {
Direction.NEXT -> {
calcPoints() calcPoints()
drawCurrentPageArea(canvas, curBitmap, mPath0) drawCurrentPageArea(canvas, curBitmap)
drawNextPageAreaAndShadow(canvas, nextBitmap) drawNextPageAreaAndShadow(canvas, nextBitmap)
drawCurrentPageShadow(canvas) drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap) drawCurrentBackArea(canvas, curBitmap)
} else { }
Direction.PREV -> {
calcPoints() calcPoints()
drawCurrentPageArea(canvas, prevBitmap, mPath0) drawCurrentPageArea(canvas, prevBitmap)
drawNextPageAreaAndShadow(canvas, curBitmap) drawNextPageAreaAndShadow(canvas, curBitmap)
drawCurrentPageShadow(canvas) drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap) 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,15 +340,14 @@ 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)
@ -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()

@ -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
) )

@ -4,7 +4,25 @@ data class TextLine(
var text: String = "", var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(), val textChars: ArrayList<TextChar> = arrayListOf(),
var lineTop: Float = 0f, var lineTop: Float = 0f,
var lineBase: Float = 0f,
var lineBottom: 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,
@ -14,6 +17,40 @@ data class TextPage(
var height: 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: Float
)

@ -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
} }

@ -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)
} }

@ -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,19 +46,26 @@ 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 {
when (requestCode) {
selectFolderRequestCode -> AppConfig.backupPath = Backup.legadoPath
else -> {
Coroutine.async { Coroutine.async {
AppConfig.backupPath = Backup.legadoPath AppConfig.backupPath = Backup.legadoPath
Backup.backup(fragment.requireContext(), path) Backup.backup(fragment.requireContext(), path)
@ -66,20 +73,22 @@ object BackupRestoreUi {
fragment.toast(R.string.backup_success) 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.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

@ -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
} }
@ -204,7 +209,6 @@ class RssSourceEditActivity :
mSoftKeyboardTool?.dismiss() mSoftKeyboardTool?.dismiss()
} }
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
val rect = Rect() val rect = Rect()
// 获取当前页面窗口的显示范围 // 获取当前页面窗口的显示范围
@ -224,5 +228,17 @@ class RssSourceEditActivity :
} }
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
}
}
}
} }
} }

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

@ -22,7 +22,11 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.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)
} }

@ -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)
} }

@ -12,8 +12,8 @@ import com.google.android.material.appbar.AppBarLayout
import io.legado.app.R import io.legado.app.R
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.getNavigationBarHeight import io.legado.app.utils.navigationBarHeight
import io.legado.app.utils.getStatusBarHeight import io.legado.app.utils.statusBarHeight
import org.jetbrains.anko.backgroundColor import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.bottomPadding import org.jetbrains.anko.bottomPadding
import org.jetbrains.anko.topPadding import org.jetbrains.anko.topPadding
@ -132,11 +132,11 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a
} }
if (a.getBoolean(R.styleable.TitleBar_fitStatusBar, true)) { if (a.getBoolean(R.styleable.TitleBar_fitStatusBar, true)) {
topPadding = context.getStatusBarHeight() topPadding = context.statusBarHeight
} }
if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) { if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) {
bottomPadding = context.getNavigationBarHeight() bottomPadding = context.navigationBarHeight
} }
backgroundColor = context.primaryColor backgroundColor = context.primaryColor

@ -20,7 +20,7 @@ class FontAdapter(context: Context, val callBack: CallBack) :
tv_font.typeface = typeface tv_font.typeface = typeface
tv_font.text = item.name tv_font.text = item.name
this.onClick { callBack.onClick(item) } this.onClick { callBack.onClick(item) }
if (item.name == callBack.curFilePath().substringAfterLast(File.separator)) { if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) {
iv_checked.visible() iv_checked.visible()
} else { } else {
iv_checked.invisible() iv_checked.invisible()
@ -38,6 +38,6 @@ class FontAdapter(context: Context, val callBack: CallBack) :
interface CallBack { interface CallBack {
fun onClick(file: File) fun onClick(file: File)
fun curFilePath(): String val curFilePath: String
} }
} }

@ -16,7 +16,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.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.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
@ -87,14 +89,15 @@ class FontSelectDialog : BaseDialogFragment(),
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_default -> { R.id.menu_default -> {
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) val requireContext = requireContext()
cb?.let { requireContext.alert(titleResource = R.string.system_typeface) {
if (it.curFontPath != "") { items(requireContext.resources.getStringArray(R.array.system_typefaces).toList()) { _, i ->
it.selectFile("") AppConfig.systemTypefaces = i
} onDefaultFontChange()
}
dismiss() dismiss()
} }
}.show()
}
R.id.menu_other -> { R.id.menu_other -> {
openFolder() openFolder()
} }
@ -103,6 +106,7 @@ class FontSelectDialog : BaseDialogFragment(),
} }
private fun openFolder() { private fun openFolder() {
launch(Main) {
alert { alert {
titleResource = R.string.select_folder titleResource = R.string.select_folder
items(resources.getStringArray(R.array.select_folder).toList()) { _, index -> items(resources.getStringArray(R.array.select_folder).toList()) { _, index ->
@ -139,10 +143,11 @@ class FontSelectDialog : BaseDialogFragment(),
} }
}.show() }.show()
} }
}
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun getFontFiles(doc: DocumentFile) { private fun getFontFiles(doc: DocumentFile) {
launch(IO) { execute {
val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri) val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri)
fontCacheFolder.listFiles()?.forEach { fontFile -> fontCacheFolder.listFiles()?.forEach { fontFile ->
var contain = false var contain = false
@ -177,6 +182,8 @@ class FontSelectDialog : BaseDialogFragment(),
} catch (e: Exception) { } catch (e: Exception) {
toast(e.localizedMessage ?: "") toast(e.localizedMessage ?: "")
} }
}.onError {
toast("getFontFiles:${it.localizedMessage}")
} }
} }
@ -204,12 +211,9 @@ class FontSelectDialog : BaseDialogFragment(),
launch(IO) { launch(IO) {
file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true) file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true)
.absolutePath.let { path -> .absolutePath.let { path ->
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) if (curFilePath != path) {
cb?.let {
if (it.curFontPath != path) {
withContext(Main) { withContext(Main) {
it.selectFile(path) callBack?.selectFile(path)
}
} }
} }
} }
@ -217,12 +221,6 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
override fun curFilePath(): String {
return (parentFragment as? CallBack)?.curFontPath
?: (activity as? CallBack)?.curFontPath
?: ""
}
override fun onFilePicked(requestCode: Int, currentPath: String) { override fun onFilePicked(requestCode: Int, currentPath: String) {
when (requestCode) { when (requestCode) {
fontFolderRequestCode -> { fontFolderRequestCode -> {
@ -255,6 +253,19 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
private fun onDefaultFontChange() {
if (curFilePath == "") {
postEvent(EventBus.UP_CONFIG, true)
} else {
callBack?.selectFile("")
}
}
override val curFilePath: String get() = callBack?.curFontPath ?: ""
private val callBack: CallBack?
get() = (parentFragment as? CallBack) ?: (activity as? CallBack)
interface CallBack { interface CallBack {
fun selectFile(path: String) fun selectFile(path: String)
val curFontPath: String val curFontPath: String

@ -24,7 +24,11 @@ import io.legado.app.utils.sp
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
class CircleImageView : AppCompatImageView { class CircleImageView(context: Context, attrs: AttributeSet) :
AppCompatImageView(
context,
attrs
) {
private val mDrawableRect = RectF() private val mDrawableRect = RectF()
private val mBorderRect = RectF() private val mBorderRect = RectF()
@ -59,10 +63,9 @@ class CircleImageView : AppCompatImageView {
private var mBorderOverlay: Boolean = false private var mBorderOverlay: Boolean = false
var isDisableCircularTransformation: Boolean = false var isDisableCircularTransformation: Boolean = false
set(disableCircularTransformation) { set(disableCircularTransformation) {
if (isDisableCircularTransformation == disableCircularTransformation) { if (field == disableCircularTransformation) {
return return
} }
field = disableCircularTransformation field = disableCircularTransformation
initializeBitmap() initializeBitmap()
} }
@ -85,7 +88,6 @@ class CircleImageView : AppCompatImageView {
if (circleBackgroundColor == mCircleBackgroundColor) { if (circleBackgroundColor == mCircleBackgroundColor) {
return return
} }
mCircleBackgroundColor = circleBackgroundColor mCircleBackgroundColor = circleBackgroundColor
mCircleBackgroundPaint.color = circleBackgroundColor mCircleBackgroundPaint.color = circleBackgroundColor
invalidate() invalidate()
@ -117,20 +119,11 @@ class CircleImageView : AppCompatImageView {
private var textColor = context.getCompatColor(R.color.tv_text_default) private var textColor = context.getCompatColor(R.color.tv_text_default)
constructor(context: Context) : super(context) { init {
init() super.setScaleType(SCALE_TYPE)
} val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView)
mBorderWidth =
@JvmOverloads a.getDimensionPixelSize(
constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(
context,
attrs,
defStyle
) {
val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0)
mBorderWidth = a.getDimensionPixelSize(
R.styleable.CircleImageView_civ_border_width, R.styleable.CircleImageView_civ_border_width,
DEFAULT_BORDER_WIDTH DEFAULT_BORDER_WIDTH
) )
@ -152,11 +145,6 @@ class CircleImageView : AppCompatImageView {
} }
a.recycle() a.recycle()
init()
}
private fun init() {
super.setScaleType(SCALE_TYPE)
mReady = true mReady = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

@ -167,9 +167,15 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
dialogIconNames?.let { dialogIconNames?.let {
val resId = context.resources val resId = context.resources
.getIdentifier(it[index].toString(), "mipmap", context.packageName) .getIdentifier(it[index].toString(), "mipmap", context.packageName)
val d = context.getCompatDrawable(resId) val d = try {
context.getCompatDrawable(resId)
} catch (e: Exception) {
null
}
d?.let {
icon.setImageDrawable(d) icon.setImageDrawable(d)
} }
}
label.isChecked = item.toString() == dialogValue label.isChecked = item.toString() == dialogValue
onClick { onClick {
onChanged?.invoke(item.toString()) onChanged?.invoke(item.toString())

@ -0,0 +1,169 @@
package io.legado.app.ui.widget.recycler
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.View
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.roundToInt
/**
* 不画最后一条分隔线
*/
class DividerNoLast(context: Context, orientation: Int) :
RecyclerView.ItemDecoration() {
companion object {
const val HORIZONTAL = LinearLayout.HORIZONTAL
const val VERTICAL = LinearLayout.VERTICAL
}
private val tag = "DividerItem"
private val attrs = intArrayOf(android.R.attr.listDivider)
private var mDivider: Drawable? = null
/**
* Current orientation. Either [.HORIZONTAL] or [.VERTICAL].
*/
private var mOrientation = 0
private val mBounds = Rect()
init {
val a = context.obtainStyledAttributes(attrs)
mDivider = a.getDrawable(0)
if (mDivider == null) {
Log.w(
tag, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()"
)
}
a.recycle()
setOrientation(orientation)
}
/**
* Sets the orientation for this divider. This should be called if
* [RecyclerView.LayoutManager] changes orientation.
*
* @param orientation [.HORIZONTAL] or [.VERTICAL]
*/
fun setOrientation(orientation: Int) {
require(!(orientation != HORIZONTAL && orientation != VERTICAL)) { "Invalid orientation. It should be either HORIZONTAL or VERTICAL" }
mOrientation = orientation
}
/**
* Sets the [Drawable] for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
fun setDrawable(drawable: Drawable) {
requireNotNull(drawable) { "Drawable cannot be null." }
mDivider = drawable
}
/**
* @return the [Drawable] for this divider.
*/
fun getDrawable(): Drawable? {
return mDivider
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null || mDivider == null) {
return
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent)
} else {
drawHorizontal(c, parent)
}
}
private fun drawVertical(
canvas: Canvas,
parent: RecyclerView
) {
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
)
} else {
left = 0
right = parent.width
}
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.bottom + child.translationY.roundToInt()
val top = bottom - mDivider!!.intrinsicHeight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val top: Int
val bottom: Int
if (parent.clipToPadding) {
top = parent.paddingTop
bottom = parent.height - parent.paddingBottom
canvas.clipRect(
parent.paddingLeft, top,
parent.width - parent.paddingRight, bottom
)
} else {
top = 0
bottom = parent.height
}
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDivider!!.intrinsicWidth
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
override fun getItemOffsets(
outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State
) {
if (mDivider == null) {
outRect[0, 0, 0] = 0
return
}
if (mOrientation == VERTICAL) {
outRect[0, 0, 0] = mDivider!!.intrinsicHeight
} else {
val childAdapterPosition = parent.getChildAdapterPosition(view)
val lastCount = parent.adapter!!.itemCount - 1
//如果不是最后一条 正常赋值 如果是最后一条 赋值为0
if (childAdapterPosition != lastCount) {
outRect[0, 0, mDivider!!.intrinsicWidth] = 0
} else {
outRect[0, 0, 0] = 0
}
}
}
}

@ -1,15 +1,16 @@
package io.legado.app.utils package io.legado.app.ui.widget.recycler
import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
class VerticalDivider(context: Context) : DividerItemDecoration(context, VERTICAL) {
fun RecyclerView.getVerticalDivider(): DividerItemDecoration { init {
return DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply {
ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let { ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let {
this.setDrawable(it) this.setDrawable(it)
} }
} }
} }

@ -121,7 +121,12 @@ object BitmapUtils {
* @throws IOException * @throws IOException
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun decodeBitmap(context: Context, fileNameInAssets: String, width: Int, height: Int): Bitmap? { fun decodeAssetsBitmap(
context: Context,
fileNameInAssets: String,
width: Int,
height: Int
): Bitmap? {
var inputStream = context.assets.open(fileNameInAssets) var inputStream = context.assets.open(fileNameInAssets)
val op = BitmapFactory.Options() val op = BitmapFactory.Options()
@ -147,7 +152,7 @@ object BitmapUtils {
//图片不被压缩 //图片不被压缩
fun convertViewToBitmap(view: View, bitmapWidth: Int, bitmapHeight: Int): Bitmap { fun convertViewToBitmap(view: View, bitmapWidth: Int, bitmapHeight: Int): Bitmap {
val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Config.ARGB_8888)
view.draw(Canvas(bitmap)) view.draw(Canvas(bitmap))
return bitmap return bitmap
} }

@ -4,6 +4,9 @@ import android.graphics.Color
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@Suppress("unused") @Suppress("unused")
object ColorUtils { object ColorUtils {
@ -53,7 +56,7 @@ object ColorUtils {
@ColorInt @ColorInt
fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int { fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int {
val alpha = Math.round(Color.alpha(color) * factor) val alpha = (Color.alpha(color) * factor).roundToInt()
val red = Color.red(color) val red = Color.red(color)
val green = Color.green(color) val green = Color.green(color)
val blue = Color.blue(color) val blue = Color.blue(color)
@ -62,7 +65,7 @@ object ColorUtils {
@ColorInt @ColorInt
fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {
val a = Math.min(255, Math.max(0, (alpha * 255).toInt())) shl 24 val a = min(255, max(0, (alpha * 255).toInt())) shl 24
val rgb = 0x00ffffff and baseColor val rgb = 0x00ffffff and baseColor
return a + rgb return a + rgb
} }

@ -1,3 +1,4 @@
@file:Suppress("unused")
package io.legado.app.utils package io.legado.app.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -6,6 +7,7 @@ import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.BatteryManager import android.os.BatteryManager
import android.provider.Settings import android.provider.Settings
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
@ -29,7 +31,6 @@ fun Context.getPrefBoolean(key: String, defValue: Boolean = false) =
fun Context.putPrefBoolean(key: String, value: Boolean = false) = fun Context.putPrefBoolean(key: String, value: Boolean = false) =
defaultSharedPreferences.edit { putBoolean(key, value) } defaultSharedPreferences.edit { putBoolean(key, value) }
fun Context.getPrefInt(key: String, defValue: Int = 0) = fun Context.getPrefInt(key: String, defValue: Int = 0) =
defaultSharedPreferences.getInt(key, defValue) defaultSharedPreferences.getInt(key, defValue)
@ -68,7 +69,8 @@ fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? =
/** /**
* 系统息屏时间 * 系统息屏时间
*/ */
fun Context.getScreenOffTime(): Int { val Context.sysScreenOffTime: Int
get() {
var screenOffTime = 0 var screenOffTime = 0
try { try {
screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
@ -78,27 +80,18 @@ fun Context.getScreenOffTime(): Int {
return screenOffTime return screenOffTime
} }
fun Context.getStatusBarHeight(): Int { val Context.statusBarHeight: Int
get() {
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId) return resources.getDimensionPixelSize(resourceId)
} }
fun Context.getNavigationBarHeight(): Int { val Context.navigationBarHeight: Int
get() {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId) return resources.getDimensionPixelSize(resourceId)
} }
fun Context.shareText(title: String, text: String) {
try {
val textIntent = Intent(Intent.ACTION_SEND)
textIntent.type = "text/plain"
textIntent.putExtra(Intent.EXTRA_TEXT, text)
startActivity(Intent.createChooser(textIntent, title))
} catch (e: Exception) {
toast(R.string.can_not_share)
}
}
@SuppressLint("SetWorldReadable") @SuppressLint("SetWorldReadable")
fun Context.shareWithQr(title: String, text: String) { fun Context.shareWithQr(title: String, text: String) {
QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L
@ -140,6 +133,9 @@ fun Context.sendToClip(text: String) {
} }
} }
/**
* 系统是否暗色主题
*/
fun Context.sysIsDarkMode(): Boolean { fun Context.sysIsDarkMode(): Boolean {
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES return mode == Configuration.UI_MODE_NIGHT_YES
@ -148,8 +144,31 @@ fun Context.sysIsDarkMode(): Boolean {
/** /**
* 获取电量 * 获取电量
*/ */
fun Context.getBettery(): Int { val Context.sysBattery: Int
get() {
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = registerReceiver(null, iFilter) val batteryStatus = registerReceiver(null, iFilter)
return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
} }
fun Context.openUrl(url: String) {
openUrl(Uri.parse(url))
}
fun Context.openUrl(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = uri
if (intent.resolveActivity(packageManager) != null) {
try {
startActivity(intent)
} catch (e: Exception) {
toast(e.localizedMessage ?: "open url error")
}
} else {
try {
startActivity(Intent.createChooser(intent, "请选择浏览器"))
} catch (e: Exception) {
toast(e.localizedMessage ?: "open url error")
}
}
}

@ -2,14 +2,13 @@ package io.legado.app.utils
import android.content.res.Resources import android.content.res.Resources
val Float.dp: Float
val Float.dp: Float // [xxhdpi](360 -> 1080)
get() = android.util.TypedValue.applyDimension( get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
) )
val Float.sp: Float // [xxhdpi](360 -> 1080) val Float.sp: Float
get() = android.util.TypedValue.applyDimension( get() = android.util.TypedValue.applyDimension(
android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics
) )

@ -1,7 +1,5 @@
package io.legado.app.utils package io.legado.app.utils
// import org.apache.commons.text.StringEscapeUtils
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true
@ -50,4 +48,14 @@ fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {
this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()
} }
fun Char?.isHAN(): Boolean {
this ?: return false
val ub: Character.UnicodeBlock = Character.UnicodeBlock.of(this) ?: return false
return ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D
|| ub === Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub === Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT
}

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"> <inset xmlns:android="http://schemas.android.com/apk/res/android">
<shape> <shape>
<size android:width="1dp" /> <size android:width="0.5dp" />
<!--分割线的颜色--> <!--分割线的颜色-->
<solid android:color="@color/bg_divider_line" /> <solid android:color="@color/bg_divider_line" />
</shape> </shape>

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

Loading…
Cancel
Save