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. 14
      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. 77
      app/src/main/java/io/legado/app/model/WebBook.kt
  24. 20
      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. 45
      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. 270
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  48. 613
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  49. 72
      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. 120
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  53. 108
      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. 162
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  56. 77
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  57. 101
      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. 56
      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. 64
      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. 111
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  91. 40
      app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt
  92. 10
      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. 79
      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")) {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.release'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@ -123,7 +124,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.3'
def room_version = '2.2.4'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
@ -139,7 +140,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.4.5'
//
def coroutines_version = '1.2.2'
def coroutines_version = '1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -175,7 +176,7 @@ dependencies {
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
//
//
implementation 'com.github.houbb:opencc4j:1.4.0'
}

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

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

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

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

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

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

@ -37,4 +37,6 @@ object PreferKey {
const val lastBackup = "lastBackup"
const val bodyIndent = "textIndent"
const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces"
}

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

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

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

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

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

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

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

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

@ -237,8 +237,14 @@ object BookHelp {
private var bookName: String? = null
private var bookOrigin: String? = null
private var replaceRules: List<ReplaceRule> = arrayListOf()
val bodyIndent
get() = " ".repeat(App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2))
var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
set(value) {
field = value
App.INSTANCE.putPrefInt(PreferKey.bodyIndent, value)
bodyIndent = " ".repeat(value)
}
var bodyIndent = " ".repeat(bodyIndentCount)
fun disposeContent(
title: String,
@ -268,7 +274,9 @@ object BookHelp {
}
}
}
c = "$title\n$c"
if (!c.substringBefore("\n").contains(title)) {
c = "$title\n$c"
}
when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(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
*/
fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
return false
return true
}
fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {

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

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

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

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

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

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

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

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

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

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

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

@ -26,10 +26,10 @@ object ReadBook {
var book: Book? = null
var inBookshelf = false
var chapterSize = 0
var callBack: CallBack? = null
var durChapterIndex = 0
var durPageIndex = 0
var isLocalBook = true
var callBack: CallBack? = null
var prevTextChapter: TextChapter? = null
var curTextChapter: TextChapter? = null
var nextTextChapter: TextChapter? = null
@ -127,7 +127,13 @@ object ReadBook {
saveRead()
}
fun curPageChanged() {
fun setPageIndex(pageIndex: Int) {
durPageIndex = pageIndex
saveRead()
curPageChanged()
}
private fun curPageChanged() {
callBack?.upPageProgress()
if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause)
@ -306,7 +312,7 @@ object ReadBook {
}
interface CallBack {
fun upContent(position: Int = 0)
fun upContent(relativePosition: Int = 0)
fun upView()
fun upPageProgress()
fun contentLoadFinish()

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

@ -11,6 +11,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
@ -41,14 +42,14 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
"contributors" -> openIntent(Intent.ACTION_VIEW, R.string.contributors_url)
"contributors" -> openUrl(R.string.contributors_url)
"update_log" -> showUpdateLog()
"check_update" -> openIntent(Intent.ACTION_VIEW, R.string.latest_release_url)
"mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com")
"git" -> openIntent(Intent.ACTION_VIEW, R.string.this_github_url)
"home_page" -> openIntent(Intent.ACTION_VIEW, R.string.home_page_url)
"license" -> openIntent(Intent.ACTION_VIEW, licenseUrl)
"disclaimer" -> openIntent(Intent.ACTION_VIEW, disclaimerUrl)
"check_update" -> openUrl(R.string.latest_release_url)
"mail" -> sendMail()
"git" -> openUrl(R.string.this_github_url)
"home_page" -> openUrl(R.string.home_page_url)
"license" -> requireContext().openUrl(licenseUrl)
"disclaimer" -> requireContext().openUrl(disclaimerUrl)
"qq" -> showQqGroups()
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
}
@ -56,17 +57,17 @@ class AboutFragment : PreferenceFragmentCompat() {
}
@Suppress("SameParameterValue")
private fun openIntent(intentName: String, @StringRes addressID: Int) {
openIntent(intentName, getString(addressID))
private fun openUrl(@StringRes addressID: Int) {
requireContext().openUrl(getString(addressID))
}
private fun openIntent(intentName: String, address: String) {
private fun sendMail() {
try {
val intent = Intent(intentName)
intent.data = Uri.parse(address)
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:kunfei.ge@gmail.com")
startActivity(intent)
} catch (e: Exception) {
toast(R.string.can_not_open)
toast(e.localizedMessage ?: "Error")
}
}

@ -9,8 +9,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R
import io.legado.app.utils.ACache
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
import org.jetbrains.anko.longToast
import java.net.URLEncoder
@ -32,10 +32,10 @@ class DonateFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
"wxZsm" -> openIntent(Intent.ACTION_VIEW, wxZsRwmUrl)
"zfbHbRwm" -> openIntent(Intent.ACTION_VIEW, zfbHbRwmUrl)
"zfbSkRwm" -> openIntent(Intent.ACTION_VIEW, zfbSkRwmUrl)
"qqSkRwm" -> openIntent(Intent.ACTION_VIEW, qqSkRwmUrl)
"wxZsm" -> requireContext().openUrl(wxZsRwmUrl)
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
@ -43,17 +43,6 @@ class DonateFragment : PreferenceFragmentCompat() {
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) {
requireContext().sendToClip("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.GroupSelectDialog
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getVerticalDivider
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.*
@ -60,7 +60,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(recycler_view.getVerticalDivider())
recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter
select_action_bar.setMainActionText(R.string.move_to_group)
@ -95,12 +95,14 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initBookData() {
booksLiveData?.removeObservers(this)
booksLiveData =
if (groupId == -1) {
App.db.bookDao().observeAll()
} else {
App.db.bookDao().observeByGroup(groupId)
when (groupId) {
-1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer {
adapter.selectedBooks.clear()
adapter.setItems(it)
upSelectCount()
})

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

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

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

@ -7,7 +7,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
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.isVisible
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.okButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.receiver.TimeElectricityReceiver
import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
@ -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.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.TextPageFactory
import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog
@ -62,6 +62,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack,
ReadBook.CallBack,
TocRegexDialog.CallBack,
ColorPickerDialogListener {
private val requestCodeChapterList = 568
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 var screenTimeOut: Long = 0
private var timeElectricityReceiver: TimeElectricityReceiver? = null
private var timeBatteryReceiver: TimeBatteryReceiver? = null
override val pageFactory: TextPageFactory get() = page_view.pageFactory
override val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -103,15 +104,15 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onResume() {
super.onResume()
upSystemUiVisibility()
timeElectricityReceiver = TimeElectricityReceiver.register(this)
timeBatteryReceiver = TimeBatteryReceiver.register(this)
page_view.upTime()
}
override fun onPause() {
super.onPause()
timeElectricityReceiver?.let {
timeBatteryReceiver?.let {
unregisterReceiver(it)
timeElectricityReceiver = null
timeBatteryReceiver = null
}
upSystemUiVisibility()
}
@ -214,6 +215,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_book_info -> ReadBook.book?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl))
}
R.id.menu_toc_regex -> TocRegexDialog.show(
supportFragmentManager,
ReadBook.book?.tocUrl
)
}
return super.onCompatOptionsItemSelected(item)
}
@ -373,14 +378,17 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
textActionMenu ?: let {
textActionMenu = TextActionMenu(this, this)
}
val x = cursor_left.x.toInt() + cursor_left.width
val y = if (cursor_left.y - statusBarHeight > ReadBookConfig.textSize.dp * 1.5 + 20.dp) {
(page_view.height - cursor_left.y + ReadBookConfig.textSize.dp * 1.5).toInt()
} else {
(page_view.height - cursor_left.y - cursor_left.height - 40.dp).toInt()
}
textActionMenu?.let { popup ->
if (!popup.isShowing) {
popup.showAtLocation(
cursor_left,
Gravity.BOTTOM or Gravity.START,
cursor_left.x.toInt() + cursor_left.width,
page_view.height - cursor_left.y.toInt() + ReadBookConfig.durConfig.textSize.dp + popup.height
)
popup.showAtLocation(cursor_left, Gravity.BOTTOM or Gravity.START, x, y)
} else {
popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT)
}
}
}
@ -393,10 +401,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/**
* 文本选择菜单操作
*/
override fun onMenuItemSelected(item: MenuItemImpl): Boolean {
when (item.itemId) {
R.id.menu_replace -> ReplaceEditDialog
.show(supportFragmentManager, pattern = selectedText)
override fun onMenuItemSelected(itemId: Int): Boolean {
when (itemId) {
R.id.menu_replace -> {
ReplaceEditDialog.show(supportFragmentManager, pattern = selectedText)
return true
}
}
return false
}
@ -406,6 +416,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/
override fun onMenuActionFinally() {
textActionMenu?.dismiss()
page_view.curPage.cancelSelect()
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 {
page_view.upContent(position)
page_view.upContent(relativePosition)
}
}
@ -492,12 +503,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
viewModel.changeTo(book)
}
override fun setPageIndex(pageIndex: Int) {
ReadBook.durPageIndex = pageIndex
ReadBook.saveRead()
ReadBook.curPageChanged()
}
override fun clickCenter() {
if (BaseReadAloudService.isRun) {
showReadAloudDialog()
@ -587,6 +592,13 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/
override fun onDialogDismissed(dialogId: Int) = Unit
override fun onTocRegexDialogResult(tocRegex: String) {
ReadBook.book?.let {
it.tocUrl = tocRegex
viewModel.loadChapterList(it)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
@ -621,7 +633,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
super.onDestroy()
mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss()
page_view.pageDelegate?.onDestroy()
page_view.onDestroy()
}
override fun observeLiveBus() {
@ -643,7 +655,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility()
page_view.upBg()
page_view.upStyle()
ChapterProvider.upStyle(ReadBookConfig.durConfig)
if (it) {
ReadBook.loadContent()
} else {
@ -680,7 +691,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upScreenTimeOut()
}
observeEvent<Boolean>(PreferKey.textSelectAble) {
page_view.upSelectAble(it)
page_view.curPage.upSelectAble(it)
}
}
@ -699,7 +710,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
Help.keepScreenOn(window, true)
return
}
val t = screenTimeOut - getScreenOffTime()
val t = screenTimeOut - sysScreenOffTime
if (t > 0) {
mHandler.removeCallbacks(keepScreenRunnable)
Help.keepScreenOn(window, true)

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

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

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

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

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

@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.ImageLoader
import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor
@ -19,7 +19,9 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.*
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefString
import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_read_book_style.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange
@ -76,8 +78,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
}
private fun initData() {
cb_share_layout.isChecked = getPrefBoolean(PreferKey.shareLayout)
requireContext().getPrefInt(PreferKey.pageAnim).let {
cb_share_layout.isChecked = ReadBookConfig.shareLayout
ReadBookConfig.pageAnim.let {
if (it >= 0 && it < rg_page_anim.childCount) {
rg_page_anim.check(rg_page_anim[it].id)
}
@ -92,14 +94,14 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
postEvent(EventBus.UP_CONFIG, true)
}
tv_title_center.onClick {
ReadBookConfig.durConfig.apply {
ReadBookConfig.apply {
titleCenter = !titleCenter
tv_title_center.isSelected = titleCenter
}
postEvent(EventBus.UP_CONFIG, true)
}
tv_text_bold.onClick {
ReadBookConfig.durConfig.apply {
ReadBookConfig.apply {
textBold = !textBold
tv_text_bold.isSelected = textBold
}
@ -113,7 +115,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
title = getString(R.string.text_indent),
items = resources.getStringArray(R.array.indent).toList()
) { _, index ->
putPrefInt(PreferKey.bodyIndent, index)
BookHelp.bodyIndentCount = index
postEvent(EventBus.UP_CONFIG, true)
}
}
@ -125,28 +127,28 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
}
}
dsb_text_size.onChanged = {
ReadBookConfig.durConfig.textSize = it + 5
ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true)
}
dsb_text_letter_spacing.onChanged = {
ReadBookConfig.durConfig.letterSpacing = (it - 50) / 100f
ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true)
}
dsb_line_size.onChanged = {
ReadBookConfig.durConfig.lineSpacingExtra = it
ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_paragraph_spacing.onChanged = {
ReadBookConfig.durConfig.paragraphSpacing = it
ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true)
}
rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) {
if (checkedId == rg_page_anim[i].id) {
requireContext().putPrefInt(PreferKey.pageAnim, i)
ReadBookConfig.pageAnim = i
val activity = activity
if (activity is ReadBookActivity) {
activity.page_view.upPageAnim(i)
activity.page_view.upPageAnim()
}
break
}
@ -154,7 +156,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
}
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
if (checkBox.isPressed) {
putPrefBoolean(PreferKey.shareLayout, isChecked)
ReadBookConfig.shareLayout = isChecked
upStyle()
postEvent(EventBus.UP_CONFIG, true)
}
}
bg0.onClick { changeBg(0) }
@ -190,7 +194,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
}
private fun upStyle() {
ReadBookConfig.durConfig.let {
ReadBookConfig.let {
tv_title_center.isSelected = it.titleCenter
tv_text_bold.isSelected = it.textBold
dsb_text_size.progress = it.textSize - 5
@ -214,12 +218,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
4 -> bg4
else -> bg0
}
ReadBookConfig.getConfig(i).apply {
when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().into(iv)
else -> iv.setImageDrawable(bgDrawable(100, 150))
}
}
iv.setImageDrawable(ReadBookConfig.getConfig(i).bgDrawable(100, 150))
}
}

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

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

@ -3,15 +3,14 @@ package io.legado.app.ui.book.read.page
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
import android.text.StaticLayout
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.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.utils.activity
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) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble)
var upView: ((TextPage) -> Unit)? = null
private val selectedPaint by lazy {
Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL
}
}
private var activityCallBack: CallBack
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble)
private var selectLineStart = 0
private var selectCharStart = 0
private var selectLineEnd = 0
private var selectCharEnd = 0
private var callBack: CallBack
private val visibleRect = RectF()
private val selectStart = arrayOf(0, 0, 0)
private val selectEnd = arrayOf(0, 0, 0)
private var textPage: TextPage = TextPage()
//滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f
private var pageOffset = 0f
private var isLastPage = false
init {
activityCallBack = activity as CallBack
callBack = activity as CallBack
contentDescription = textPage.text
}
@ -48,72 +47,114 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
invalidate()
}
fun upVisibleRect() {
visibleRect.set(
ChapterProvider.paddingLeft.toFloat(),
ChapterProvider.paddingTop.toFloat(),
ChapterProvider.visibleRight.toFloat(),
ChapterProvider.visibleBottom.toFloat()
)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
ReadBookConfig.durConfig.let {
ChapterProvider.viewWidth = w
ChapterProvider.viewHeight = h
ChapterProvider.upSize(ReadBookConfig.durConfig)
}
ChapterProvider.viewWidth = w
ChapterProvider.viewHeight = h
ChapterProvider.upSize()
upVisibleRect()
textPage.format()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (textPage.textLines.isEmpty()) {
drawMsg(canvas, textPage.text)
} else {
drawHorizontalPage(canvas)
}
canvas.clipRect(visibleRect)
drawPage(canvas)
}
@Suppress("DEPRECATION")
private fun drawMsg(canvas: Canvas, msg: String) {
val layout = StaticLayout(
msg, ChapterProvider.contentPaint, width,
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
)
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)
/**
* 绘制页面
*/
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
textPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
val nextPage = relativePage(1)
relativeOffset = relativeOffset(1)
nextPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) {
relativePage(2).textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
}
}
private fun drawHorizontalPage(canvas: Canvas) {
textPage.textLines.forEach { textLine ->
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
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) {
if (mOffset == 0f) return
var offset = mOffset
if (offset > maxScrollOffset) {
offset = maxScrollOffset
@ -121,158 +162,430 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
offset = -maxScrollOffset
}
if (!isLastPage || offset < 0) {
pageOffset += offset
isLastPage = false
}
// 首页
if (pageOffset < 0 && ReadBook.durChapterIndex == 0 && ReadBook.durPageIndex == 0) {
pageOffset = 0f
pageOffset += offset
if (pageOffset > 0) {
if (!pageFactory.hasPrev()) {
pageOffset = 0f
} else {
pageFactory.moveToPrev()
textPage = pageFactory.currentPage
pageOffset -= textPage.height
upView?.invoke(textPage)
}
} else if (pageOffset < -textPage.height) {
if (!pageFactory.hasNext()) {
pageOffset = -textPage.height.toFloat()
} else {
pageOffset += textPage.height
pageFactory.moveToNext()
textPage = pageFactory.currentPage
upView?.invoke(textPage)
}
}
invalidate()
}
fun resetPageOffset() {
pageOffset = 0f
}
fun selectText(x: Float, y: Float): 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()) {
if (y > textLine.lineTop && y < textLine.lineBottom) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectLineStart = lineIndex
selectCharStart = charIndex
selectLineEnd = lineIndex
selectCharEnd = charIndex
upSelectedStart(
textChar.leftBottomPosition.x,
textChar.leftBottomPosition.y
)
upSelectedEnd(
textChar.rightTopPosition.x,
textChar.leftBottomPosition.y
)
return true
selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 0
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(0, lineIndex, charIndex)
return
}
}
break
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
val nextPage = relativePage(1)
for ((lineIndex, textLine) in nextPage.textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(1, lineIndex, charIndex)
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
textChar.selected = true
invalidate()
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(2, lineIndex, charIndex)
return
}
}
return
}
}
return false
}
/**
* 开始选择符移动
*/
fun selectStartMove(x: Float, y: Float) {
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) {
if (selectLineStart != lineIndex || selectCharStart != charIndex) {
selectLineStart = lineIndex
selectCharStart = charIndex
upSelectedStart(
textChar.leftBottomPosition.x,
textChar.leftBottomPosition.y
)
upSelectChars(textPage)
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 0 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(0, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 1 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(1, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
break
return
}
}
break
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectStart[0] != 2 || selectStart[1] != lineIndex || selectStart[2] != charIndex) {
if (selectToInt(2, lineIndex, charIndex) > selectToInt(selectEnd)) {
return
}
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
}
/**
* 结束选择符移动
*/
fun selectEndMove(x: Float, y: Float) {
if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
if (y > textLine.lineTop && y < textLine.lineBottom) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) {
if (selectLineEnd != lineIndex || selectCharEnd != charIndex) {
selectLineEnd = lineIndex
selectCharEnd = charIndex
upSelectedEnd(
textChar.rightTopPosition.x,
textChar.leftBottomPosition.y
)
upSelectChars(textPage)
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 0 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(0, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 0
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
break
return
}
}
break
return
}
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
relativeOffset = relativeOffset(1)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 1 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(1, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
relativeOffset = relativeOffset(2)
if (relativeOffset >= ChapterProvider.visibleHeight) return
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) {
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
if (x > textChar.start && x < textChar.end) {
if (selectEnd[0] != 2 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) {
if (selectToInt(2, lineIndex, charIndex) < selectToInt(selectStart)) {
return
}
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars()
}
return
}
}
return
}
}
}
private fun upSelectChars(textPage: TextPage) {
for ((lineIndex, textLine) in textPage.textLines.withIndex()) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
textChar.selected =
if (lineIndex == selectLineStart && lineIndex == selectLineEnd) {
charIndex in selectCharStart..selectCharEnd
} else if (lineIndex == selectLineStart) {
charIndex >= selectCharStart
} else if (lineIndex == selectLineEnd) {
charIndex <= selectCharEnd
} else {
lineIndex in (selectLineStart + 1) until selectLineEnd
}
/**
* 选择开始文字
*/
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectStart[0] = relativePage
selectStart[1] = lineIndex
selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
/**
* 选择结束文字
*/
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
selectEnd[0] = relativePage
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars()
}
private fun upSelectChars() {
val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) {
for ((charIndex, textChar) in textLine.textChars.withIndex()) {
textChar.selected =
if (relativePos == selectStart[0]
&& relativePos == selectEnd[0]
&& lineIndex == selectStart[1]
&& lineIndex == selectEnd[1]
) {
charIndex in selectStart[2]..selectEnd[2]
} else if (relativePos == selectStart[0] && lineIndex == selectStart[1]) {
charIndex >= selectStart[2]
} else if (relativePos == selectEnd[0] && lineIndex == selectEnd[1]) {
charIndex <= selectEnd[2]
} else if (relativePos == selectStart[0] && relativePos == selectEnd[0]) {
lineIndex in (selectStart[1] + 1) until selectEnd[1]
} else if (relativePos == selectStart[0]) {
lineIndex > selectStart[1]
} else if (relativePos == selectEnd[0]) {
lineIndex < selectEnd[1]
} else {
relativePos in selectStart[0] + 1 until selectEnd[0]
}
}
}
}
invalidate()
}
private fun upSelectedStart(x: Float, y: Float) {
activityCallBack.upSelectedStart(x, y + activityCallBack.headerHeight)
callBack.upSelectedStart(x, y + callBack.headerHeight)
}
private fun upSelectedEnd(x: Float, y: Float) {
activityCallBack.upSelectedEnd(x, y + activityCallBack.headerHeight)
callBack.upSelectedEnd(x, y + callBack.headerHeight)
}
fun cancelSelect() {
textPage.textLines.forEach { textLine ->
textLine.textChars.forEach {
it.selected = false
val last = if (ReadBookConfig.isScroll) 2 else 0
for (relativePos in 0..last) {
relativePage(relativePos).textLines.forEach { textLine ->
textLine.textChars.forEach {
it.selected = false
}
}
}
invalidate()
activityCallBack.onCancelSelect()
callBack.onCancelSelect()
}
val selectedText: String
get() {
val stringBuilder = StringBuilder()
for (lineIndex in selectLineStart..selectLineEnd) {
if (lineIndex == selectLineStart && lineIndex == selectLineEnd) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectCharStart,
selectCharEnd + 1
)
)
} else if (lineIndex == selectLineStart) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectCharStart
)
)
} else if (lineIndex == selectLineEnd) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectCharEnd + 1)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
for (relativePos in selectStart[0]..selectEnd[0]) {
val textPage = relativePage(relativePos)
if (relativePos == selectStart[0] && relativePos == selectEnd[0]) {
for (lineIndex in selectStart[1]..selectEnd[1]) {
if (lineIndex == selectStart[1] && lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2],
selectEnd[2] + 1
)
)
} else if (lineIndex == selectStart[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2]
)
)
} else if (lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos == selectStart[0]) {
for (lineIndex in selectStart[1] until relativePage(relativePos).textLines.size) {
if (lineIndex == selectStart[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(
selectStart[2]
)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos == selectEnd[0]) {
for (lineIndex in 0..selectEnd[1]) {
if (lineIndex == selectEnd[1]) {
stringBuilder.append(
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1)
)
} else {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
} else if (relativePos in selectStart[0] + 1 until selectEnd[0]) {
for (lineIndex in selectStart[1]..selectEnd[1]) {
stringBuilder.append(textPage.textLines[lineIndex].text)
}
}
}
return stringBuilder.toString()
}
private fun selectToInt(page: Int, line: Int, char: Int): Int {
return page * 1000000 + line * 100000 + char
}
private fun selectToInt(select: Array<Int>): Int {
return select[0] * 1000000 + select[1] * 100000 + select[2]
}
private fun relativeOffset(relativePos: Int): Float {
return when (relativePos) {
0 -> pageOffset
1 -> pageOffset + textPage.height
else -> pageOffset + textPage.height + pageFactory.nextPage.height
}
}
private fun relativePage(relativePos: Int): TextPage {
return when (relativePos) {
0 -> textPage
1 -> pageFactory.nextPage
else -> pageFactory.nextPagePlus
}
}
interface CallBack {
fun upSelectedStart(x: Float, y: Float)
fun upSelectedEnd(x: Float, y: Float)
fun onCancelSelect()
val headerHeight: Int
val pageFactory: TextPageFactory
}
}

@ -5,9 +5,10 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.view.MotionEvent
import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.*
@ -16,8 +17,6 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) {
var callBack: CallBack? = null
private var pageSize: Int = 0
init {
//设置背景防止切换背景时文字重叠
@ -26,18 +25,23 @@ class ContentView(context: Context) : FrameLayout(context) {
upStyle()
upTime()
content_text_view.upView = {
tv_bottom_left.text = it.title
setPageIndex(it.index, it.pageSize)
}
}
fun upStyle() {
ReadBookConfig.durConfig.apply {
ReadBookConfig.apply {
tv_top_left.typeface = ChapterProvider.typeface
tv_top_right.typeface = ChapterProvider.typeface
tv_bottom_left.typeface = ChapterProvider.typeface
tv_bottom_right.typeface = ChapterProvider.typeface
//显示状态栏时隐藏header
if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) {
ll_header.layoutParams =
ll_header.layoutParams.apply { height = context.getStatusBarHeight() }
if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams.apply {
height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding(
headerPaddingLeft.dp,
headerPaddingTop.dp,
@ -48,21 +52,16 @@ class ContentView(context: Context) : FrameLayout(context) {
page_panel.setPadding(0, 0, 0, 0)
} else {
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(
footerPaddingLeft.dp,
footerPaddingTop.dp,
footerPaddingRight.dp,
footerPaddingBottom.dp
)
textColor().let {
content_text_view.upVisibleRect()
durConfig.textColor().let {
tv_top_left.setTextColor(it)
tv_top_right.setTextColor(it)
tv_bottom_left.setTextColor(it)
@ -73,10 +72,10 @@ class ContentView(context: Context) : FrameLayout(context) {
val headerHeight: Int
get() {
return if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) {
return if (ReadBookConfig.hideStatusBar) {
ll_header.height
} 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)
}
fun setContent(textPage: TextPage?) {
if (textPage != null) {
content_text_view.setContent(textPage)
tv_bottom_left.text = textPage.title
pageSize = textPage.pageSize
setPageIndex(textPage.index)
fun setContent(textPage: TextPage) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage)
}
fun resetPageOffset() {
content_text_view.resetPageOffset()
}
@SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?) {
fun setPageIndex(pageIndex: Int?, pageSize: Int) {
pageIndex?.let {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}"
}
@ -116,27 +121,34 @@ class ContentView(context: Context) : FrameLayout(context) {
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
return content_text_view.selectText(e.x, y)
return content_text_view.selectText(e.x, y, select)
}
fun selectStartMove(x: Float, y: Float) {
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) {
content_text_view.selectEndMove(x, y - headerHeight)
}
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) {
content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
fun cancelSelect() {
content_text_view.cancelSelect()
}
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
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChapter
interface DataSource {
val isScrollDelegate: Boolean
val pageIndex: Int
val pageIndex: Int get() = ReadBook.durChapterPos()
fun setPageIndex(pageIndex: Int)
val currentChapter: TextChapter?
fun getChapterPosition(): Int
val nextChapter: TextChapter?
fun getCurrentChapter(): TextChapter?
fun getNextChapter(): TextChapter?
fun getPreviousChapter(): TextChapter?
val prevChapter: TextChapter?
fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean
}

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

@ -6,21 +6,18 @@ import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt
class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs),
ContentView.CallBack,
DataSource {
var callBack: CallBack? = null
var pageFactory: TextPageFactory? = null
var callBack: CallBack
var pageFactory: TextPageFactory
var pageDelegate: PageDelegate? = null
var prevPage: ContentView
@ -28,7 +25,7 @@ class PageView(context: Context, attrs: AttributeSet) :
var nextPage: ContentView
init {
callBack = activity as? CallBack
callBack = activity as CallBack
prevPage = ContentView(context)
addView(prevPage)
nextPage = ContentView(context)
@ -38,8 +35,7 @@ class PageView(context: Context, attrs: AttributeSet) :
upBg()
setWillNotDraw(false)
pageFactory = TextPageFactory(this)
upPageAnim(context.getPrefInt(PreferKey.pageAnim))
curPage.callBack = this
upPageAnim()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@ -66,28 +62,34 @@ class PageView(context: Context, attrs: AttributeSet) :
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
callBack?.screenOffTimerStart()
return pageDelegate?.onTouch(event) ?: super.onTouchEvent(event)
pageDelegate?.onTouch(event)
callBack.screenOffTimerStart()
return true
}
fun onDestroy() {
pageDelegate?.onDestroy()
curPage.cancelSelect()
}
fun fillPage(direction: PageDelegate.Direction) {
when (direction) {
PageDelegate.Direction.PREV -> {
pageFactory?.moveToPrevious()
pageFactory.moveToPrev()
upContent()
}
PageDelegate.Direction.NEXT -> {
pageFactory?.moveToNext()
pageFactory.moveToNext()
upContent()
}
else -> Unit
}
}
fun upPageAnim(pageAnim: Int) {
fun upPageAnim() {
pageDelegate?.onDestroy()
pageDelegate = null
pageDelegate = when (pageAnim) {
pageDelegate = when (ReadBookConfig.pageAnim) {
0 -> CoverPageDelegate(this)
1 -> SlidePageDelegate(this)
2 -> SimulationPageDelegate(this)
@ -97,19 +99,21 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent()
}
fun upContent(position: Int = 0) {
pageFactory?.let {
when (position) {
-1 -> prevPage.setContent(it.previousPage())
1 -> nextPage.setContent(it.nextPage())
fun upContent(relativePosition: Int = 0) {
if (ReadBookConfig.isScroll) {
curPage.setContent(pageFactory.currentPage)
} else {
when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage)
1 -> nextPage.setContent(pageFactory.nextPage)
else -> {
curPage.setContent(it.currentPage())
nextPage.setContent(it.nextPage())
prevPage.setContent(it.previousPage())
curPage.setContent(pageFactory.currentPage)
nextPage.setContent(pageFactory.nextPage)
prevPage.setContent(pageFactory.prevPage)
}
}
}
callBack?.screenOffTimerStart()
callBack.screenOffTimerStart()
}
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() {
ChapterProvider.upStyle()
curPage.upStyle()
prevPage.upStyle()
nextPage.upStyle()
@ -160,30 +160,19 @@ class PageView(context: Context, attrs: AttributeSet) :
nextPage.upBattery(battery)
}
override val isScrollDelegate: Boolean
get() = pageDelegate is ScrollPageDelegate
override val pageIndex: Int
get() = ReadBook.durChapterPos()
override fun setPageIndex(pageIndex: Int) {
callBack?.setPageIndex(pageIndex)
}
override fun getChapterPosition(): Int {
return ReadBook.durChapterIndex
}
override fun getCurrentChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null
override val currentChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
}
override fun getNextChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null
override val nextChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(1) else null
}
override fun getPreviousChapter(): TextChapter? {
return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null
override val prevChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null
}
override fun hasNextChapter(): Boolean {
@ -191,45 +180,12 @@ class PageView(context: Context, attrs: AttributeSet) :
}
override fun hasPrevChapter(): Boolean {
callBack?.let {
return ReadBook.durChapterIndex > 0
}
return false
}
override fun scrollToLine(line: Int) {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
val pageIndex = it.getPageIndex(line)
curPage.setPageIndex(pageIndex)
callBack?.setPageIndex(pageIndex)
}
}
}
override fun scrollToLast() {
if (isScrollDelegate) {
ReadBook.textChapter()?.let {
callBack?.setPageIndex(it.lastIndex())
curPage.setPageIndex(it.lastIndex())
}
}
return ReadBook.durChapterIndex > 0
}
interface CallBack {
val isInitFinish: Boolean
/**
* 保存页数
*/
fun setPageIndex(pageIndex: Int)
/**
* 点击屏幕中间
*/
fun clickCenter()
fun screenOffTimerStart()
}
}

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

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

@ -6,15 +6,14 @@ import android.graphics.Canvas
import android.graphics.RectF
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import com.google.android.material.snackbar.Snackbar
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.screenshot
import kotlin.math.abs
@ -25,6 +24,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
pageView.width * 0.66f, pageView.height * 0.66f
)
protected val context: Context = pageView.context
protected val slop = ViewConfiguration.get(context).scaledTouchSlop
//起始点
protected var startX: Float = 0f
protected var startY: Float = 0f
@ -35,35 +35,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
protected var touchX: Float = 0f
protected var touchY: Float = 0f
protected val nextPage: ContentView
get() = pageView.nextPage
protected val curPage: ContentView
get() = pageView.curPage
protected val prevPage: ContentView
get() = pageView.prevPage
protected val nextPage: ContentView get() = pageView.nextPage
protected val curPage: ContentView get() = pageView.curPage
protected val prevPage: ContentView get() = pageView.prevPage
protected var bitmap: Bitmap? = null
protected var viewWidth: Int = pageView.width
protected var viewHeight: Int = pageView.height
//textView在顶端或低端
protected var atTop: Boolean = false
protected var atBottom: Boolean = false
private val snackBar: Snackbar by lazy {
Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT)
}
private val scroller: Scroller by lazy {
Scroller(pageView.context, FastOutLinearInInterpolator())
Scroller(pageView.context, DecelerateInterpolator())
}
private val detector: GestureDetector by lazy {
GestureDetector(pageView.context, this).apply {
setIsLongpressEnabled(context.getPrefBoolean(PreferKey.textSelectAble))
}
GestureDetector(pageView.context, this)
}
var isMoved = false
@ -75,6 +65,15 @@ abstract class PageDelegate(protected val pageView: PageView) :
var isRunning = false
var isStarted = false
var isTextSelected = false
var selectedOnDown = false
var firstRelativePage = 0
var firstLineIndex: Int = 0
var firstCharIndex: Int = 0
init {
curPage.resetPageOffset()
}
open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x
@ -85,7 +84,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
touchY = y
if (invalidate) {
invalidate()
pageView.invalidate()
}
}
@ -96,20 +95,12 @@ abstract class PageDelegate(protected val pageView: PageView) :
touchY = y
if (invalidate) {
invalidate()
pageView.invalidate()
}
onScroll()
}
fun upSelectAble(selectAble: Boolean) {
detector.setIsLongpressEnabled(selectAble)
}
protected fun invalidate() {
pageView.invalidate()
}
open fun fling(
startX: Int, startY: Int, velocityX: Int, velocityY: 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)
isRunning = true
isStarted = true
invalidate()
pageView.invalidate()
}
protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) {
@ -130,28 +121,21 @@ abstract class PageDelegate(protected val pageView: PageView) :
)
isRunning = true
isStarted = true
invalidate()
pageView.invalidate()
}
protected fun stopScroll() {
private fun stopScroll() {
isRunning = false
isStarted = false
invalidate()
if (pageView.isScrollDelegate) {
pageView.postDelayed({
bitmap?.recycle()
bitmap = null
}, 100)
} else {
bitmap?.recycle()
bitmap = null
}
pageView.invalidate()
bitmap?.recycle()
bitmap = null
}
fun setViewSize(width: Int, height: Int) {
open fun setViewSize(width: Int, height: Int) {
viewWidth = width
viewHeight = height
invalidate()
pageView.invalidate()
centerRectF.set(
width * 0.33f, height * 0.33f,
width * 0.66f, height * 0.66f
@ -162,7 +146,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
setTouchPoint(scroller.finalX.toFloat(), scroller.finalY.toFloat(), false)
onAnimStop()
stopScroll()
}
@ -202,14 +185,13 @@ abstract class PageDelegate(protected val pageView: PageView) :
onAnimStart()
}
abstract fun onAnimStart()//scroller start
open fun onAnimStart() {}//scroller start
abstract fun onDraw(canvas: Canvas)//绘制
open fun onDraw(canvas: Canvas) {}//绘制
abstract fun onAnimStop()//scroller finish
open fun onAnimStop() {}//scroller finish
open fun onScroll() {//移动contentView, slidePage
}
open fun onScroll() {}//移动contentView, slidePage
@CallSuper
open fun setDirection(direction: Direction) {
@ -228,9 +210,17 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 触摸事件处理
*/
@CallSuper
open fun onTouch(event: MotionEvent): Boolean {
if (isStarted) return false
return detector.onTouchEvent(event)
open fun onTouch(event: MotionEvent) {
if (isStarted) return
if (!detector.onTouchEvent(event)) {
//GestureDetector.onFling小幅移动不会触发,所以要自己判断
if (event.action == MotionEvent.ACTION_UP && isMoved) {
if (selectedOnDown) {
selectedOnDown = false
}
if (!noNext) onAnimStart()
}
}
}
/**
@ -239,6 +229,8 @@ abstract class PageDelegate(protected val pageView: PageView) :
override fun onDown(e: MotionEvent): Boolean {
if (isTextSelected) {
curPage.cancelSelect()
isTextSelected = false
selectedOnDown = true
}
//是否移动
isMoved = false
@ -259,29 +251,25 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 单击
*/
override fun onSingleTapUp(e: MotionEvent): Boolean {
if (isTextSelected) {
isTextSelected = false
if (selectedOnDown) {
selectedOnDown = false
return true
}
val x = e.x
val y = e.y
if (centerRectF.contains(x, y)) {
pageView.callBack?.clickCenter()
pageView.callBack.clickCenter()
setTouchPoint(x, y)
} else {
if (x > viewWidth / 2 ||
AppConfig.clickAllNext
) {
//设置动画方向
if (!hasNext()) {
return true
}
if (!hasNext()) return true
setDirection(Direction.NEXT)
setBitmap()
} else {
if (!hasPrev()) {
return true
}
if (!hasPrev()) return true
setDirection(Direction.PREV)
setBitmap()
}
@ -295,30 +283,50 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 长按选择
*/
override fun onLongPress(e: MotionEvent) {
isTextSelected = curPage.selectText(e)
curPage.selectText(e) { relativePage, lineIndex, charIndex ->
isTextSelected = true
firstRelativePage = relativePage
firstLineIndex = lineIndex
firstCharIndex = charIndex
}
}
/**
* 移动结束
*/
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
if (isTextSelected) {
isTextSelected = false
protected fun selectText(event: MotionEvent) {
curPage.selectText(event) { relativePage, lineIndex, charIndex ->
when {
relativePage > firstRelativePage -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
relativePage < firstRelativePage -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
lineIndex > firstLineIndex -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
lineIndex < firstLineIndex -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
charIndex > firstCharIndex -> {
curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex)
}
else -> {
curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex)
curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex)
}
}
}
if (!noNext) onAnimStart()
return true
}
/**
* 判断是否有上一页
*/
fun hasPrev(): Boolean {
val hasPrev = pageView.pageFactory?.hasPrev() == true
val hasPrev = pageView.pageFactory.hasPrev()
if (!hasPrev) {
if (!snackBar.isShown) {
snackBar.setText("没有上一页")
@ -332,7 +340,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
* 判断是否有下一页
*/
fun hasNext(): Boolean {
val hasNext = pageView.pageFactory?.hasNext() == true
val hasNext = pageView.pageFactory.hasNext()
if (!hasNext) {
if (!snackBar.isShown) {
snackBar.setText("没有下一页")

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

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

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

@ -4,7 +4,25 @@ data class TextLine(
var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(),
var lineTop: Float = 0f,
var lineBase: Float = 0f,
var lineBottom: Float = 0f,
val isTitle: Boolean = false,
var isReadAloud: Boolean = false
)
) {
fun addTextChar(charData: String, start: Float, end: Float) {
textChars.add(TextChar(charData, start = start, end = end))
}
fun getTextCharAt(index: Int): TextChar {
return textChars[index]
}
fun getTextCharReverseAt(index: Int): TextChar {
return textChars[textChars.lastIndex - index]
}
fun getTextCharsCount(): Int {
return textChars.size
}
}

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

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

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

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

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

@ -25,9 +25,13 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.*
import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.share
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs
@ -84,9 +88,7 @@ class BookSourceEditActivity :
}
}
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getSource())?.let { sourceStr ->
shareText(getString(R.string.share_book_source), sourceStr)
}
R.id.menu_share_str -> GSON.toJson(getSource())?.let { share(it) }
R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_book_source), sourceStr)
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -12,8 +12,8 @@ import com.google.android.material.appbar.AppBarLayout
import io.legado.app.R
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.activity
import io.legado.app.utils.getNavigationBarHeight
import io.legado.app.utils.getStatusBarHeight
import io.legado.app.utils.navigationBarHeight
import io.legado.app.utils.statusBarHeight
import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.bottomPadding
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)) {
topPadding = context.getStatusBarHeight()
topPadding = context.statusBarHeight
}
if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) {
bottomPadding = context.getNavigationBarHeight()
bottomPadding = context.navigationBarHeight
}
backgroundColor = context.primaryColor

@ -20,7 +20,7 @@ class FontAdapter(context: Context, val callBack: CallBack) :
tv_font.typeface = typeface
tv_font.text = item.name
this.onClick { callBack.onClick(item) }
if (item.name == callBack.curFilePath().substringAfterLast(File.separator)) {
if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) {
iv_checked.visible()
} else {
iv_checked.invisible()
@ -38,6 +38,6 @@ class FontAdapter(context: Context, val callBack: CallBack) :
interface CallBack {
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.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
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.PermissionsCompat
import io.legado.app.lib.dialogs.alert
@ -87,13 +89,14 @@ class FontSelectDialog : BaseDialogFragment(),
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_default -> {
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack)
cb?.let {
if (it.curFontPath != "") {
it.selectFile("")
val requireContext = requireContext()
requireContext.alert(titleResource = R.string.system_typeface) {
items(requireContext.resources.getStringArray(R.array.system_typefaces).toList()) { _, i ->
AppConfig.systemTypefaces = i
onDefaultFontChange()
dismiss()
}
}
dismiss()
}.show()
}
R.id.menu_other -> {
openFolder()
@ -103,46 +106,48 @@ class FontSelectDialog : BaseDialogFragment(),
}
private fun openFolder() {
alert {
titleResource = R.string.select_folder
items(resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) {
0 -> {
val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts"
putPrefString(PreferKey.fontFolder, path)
getFontFilesByPermission(path)
}
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, fontFolderRequestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
requireContext().toast(e.localizedMessage ?: "ERROR")
launch(Main) {
alert {
titleResource = R.string.select_folder
items(resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) {
0 -> {
val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts"
putPrefString(PreferKey.fontFolder, path)
getFontFilesByPermission(path)
}
}
2 -> {
PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
FileChooserDialog.show(
childFragmentManager,
fontFolderRequestCode,
mode = FileChooserDialog.DIRECTORY
)
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, fontFolderRequestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
requireContext().toast(e.localizedMessage ?: "ERROR")
}
.request()
}
2 -> {
PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
FileChooserDialog.show(
childFragmentManager,
fontFolderRequestCode,
mode = FileChooserDialog.DIRECTORY
)
}
.request()
}
}
}
}
}.show()
}.show()
}
}
@SuppressLint("DefaultLocale")
private fun getFontFiles(doc: DocumentFile) {
launch(IO) {
execute {
val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri)
fontCacheFolder.listFiles()?.forEach { fontFile ->
var contain = false
@ -177,6 +182,8 @@ class FontSelectDialog : BaseDialogFragment(),
} catch (e: Exception) {
toast(e.localizedMessage ?: "")
}
}.onError {
toast("getFontFiles:${it.localizedMessage}")
}
}
@ -204,12 +211,9 @@ class FontSelectDialog : BaseDialogFragment(),
launch(IO) {
file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true)
.absolutePath.let { path ->
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack)
cb?.let {
if (it.curFontPath != path) {
withContext(Main) {
it.selectFile(path)
}
if (curFilePath != path) {
withContext(Main) {
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) {
when (requestCode) {
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 {
fun selectFile(path: String)
val curFontPath: String

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

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

@ -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.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
class VerticalDivider(context: Context) : DividerItemDecoration(context, VERTICAL) {
fun RecyclerView.getVerticalDivider(): DividerItemDecoration {
return DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply {
init {
ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let {
this.setDrawable(it)
}
}
}

@ -121,7 +121,12 @@ object BitmapUtils {
* @throws IOException
*/
@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)
val op = BitmapFactory.Options()
@ -147,7 +152,7 @@ object BitmapUtils {
//图片不被压缩
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))
return bitmap
}

@ -4,6 +4,9 @@ import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@Suppress("unused")
object ColorUtils {
@ -53,7 +56,7 @@ object ColorUtils {
@ColorInt
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 green = Color.green(color)
val blue = Color.blue(color)
@ -62,7 +65,7 @@ object ColorUtils {
@ColorInt
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
return a + rgb
}

@ -1,3 +1,4 @@
@file:Suppress("unused")
package io.legado.app.utils
import android.annotation.SuppressLint
@ -6,6 +7,7 @@ import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.BatteryManager
import android.provider.Settings
import androidx.annotation.ColorRes
@ -29,7 +31,6 @@ fun Context.getPrefBoolean(key: String, defValue: Boolean = false) =
fun Context.putPrefBoolean(key: String, value: Boolean = false) =
defaultSharedPreferences.edit { putBoolean(key, value) }
fun Context.getPrefInt(key: String, defValue: Int = 0) =
defaultSharedPreferences.getInt(key, defValue)
@ -68,35 +69,27 @@ fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? =
/**
* 系统息屏时间
*/
fun Context.getScreenOffTime(): Int {
var screenOffTime = 0
try {
screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
} catch (e: Exception) {
e.printStackTrace()
}
return screenOffTime
}
fun Context.getStatusBarHeight(): Int {
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId)
val Context.sysScreenOffTime: Int
get() {
var screenOffTime = 0
try {
screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
} catch (e: Exception) {
e.printStackTrace()
}
return screenOffTime
}
fun Context.getNavigationBarHeight(): Int {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId)
val Context.statusBarHeight: Int
get() {
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
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)
}
val Context.navigationBarHeight: Int
get() {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId)
}
@SuppressLint("SetWorldReadable")
@ -140,6 +133,9 @@ fun Context.sendToClip(text: String) {
}
}
/**
* 系统是否暗色主题
*/
fun Context.sysIsDarkMode(): Boolean {
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES
@ -148,8 +144,31 @@ fun Context.sysIsDarkMode(): Boolean {
/**
* 获取电量
*/
fun Context.getBettery(): Int {
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = registerReceiver(null, iFilter)
return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val Context.sysBattery: Int
get() {
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = registerReceiver(null, iFilter)
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
val Float.dp: Float // [xxhdpi](360 -> 1080)
val Float.dp: Float
get() = android.util.TypedValue.applyDimension(
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(
android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics
)

@ -1,7 +1,5 @@
package io.legado.app.utils
// import org.apache.commons.text.StringEscapeUtils
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
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()
}
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"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android">
<shape>
<size android:width="1dp" />
<size android:width="0.5dp" />
<!--分割线的颜色-->
<solid android:color="@color/bg_divider_line" />
</shape>

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

Loading…
Cancel
Save