Merge pull request #32 from gedoor/master

up
pull/379/head
口口吕 5 years ago committed by GitHub
commit b92ab3a7af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/src/main/assets/help.md
  2. 13
      app/src/main/assets/updateLog.md
  3. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  4. 3
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  5. 40
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  6. 3
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  7. 1
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  8. 12
      app/src/main/java/io/legado/app/model/Debug.kt
  9. 93
      app/src/main/java/io/legado/app/model/SearchBookModel.kt
  10. 7
      app/src/main/java/io/legado/app/model/WebBook.kt
  11. 32
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  12. 4
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  13. 2
      app/src/main/java/io/legado/app/service/DownloadService.kt
  14. 7
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  15. 2
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  16. 7
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  17. 13
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  18. 2
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  19. 4
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  20. 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  21. 16
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  22. 7
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  23. 2
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  24. 88
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  25. 2
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  26. 2
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  27. 4
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  28. 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  29. 8
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt
  30. 17
      app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt
  31. 2
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  32. 110
      app/src/main/java/io/legado/app/ui/widget/SearchView.kt
  33. 185
      app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt
  34. 9
      app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt
  35. 21
      app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt
  36. 6
      app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt
  37. 7
      app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt
  38. 85
      app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt
  39. 27
      app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt
  40. 22
      app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt
  41. 10
      app/src/main/res/drawable/bg_prefs_color.xml
  42. 15
      app/src/main/res/drawable/ic_cfg_about.xml
  43. 9
      app/src/main/res/drawable/ic_cfg_backup.xml
  44. 5
      app/src/main/res/drawable/ic_cfg_e_lnk.xml
  45. 12
      app/src/main/res/drawable/ic_cfg_jz.xml
  46. 12
      app/src/main/res/drawable/ic_cfg_other.xml
  47. 12
      app/src/main/res/drawable/ic_cfg_replace.xml
  48. 12
      app/src/main/res/drawable/ic_cfg_source.xml
  49. 9
      app/src/main/res/drawable/ic_cfg_theme.xml
  50. 9
      app/src/main/res/drawable/ic_cfg_web.xml
  51. 19
      app/src/main/res/drawable/ic_help.xml
  52. 2
      app/src/main/res/drawable/ic_launcher1_b.xml
  53. 8
      app/src/main/res/drawable/ic_search_hint.xml
  54. 2
      app/src/main/res/layout/activity_main.xml
  55. 2
      app/src/main/res/layout/activity_rss_read.xml
  56. 54
      app/src/main/res/layout/item_bookshelf_grid.xml
  57. 65
      app/src/main/res/layout/view_preference.xml
  58. 29
      app/src/main/res/layout/view_preference_category.xml
  59. 10
      app/src/main/res/layout/view_search.xml
  60. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  61. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  62. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  63. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  64. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  65. 1
      app/src/main/res/values-night/colors.xml
  66. 13
      app/src/main/res/values/attrs.xml
  67. 1
      app/src/main/res/values/colors.xml
  68. 5
      app/src/main/res/values/strings.xml
  69. 30
      app/src/main/res/xml/about.xml
  70. 25
      app/src/main/res/xml/donate.xml
  71. 24
      app/src/main/res/xml/pref_config_backup.xml
  72. 16
      app/src/main/res/xml/pref_config_other.xml
  73. 14
      app/src/main/res/xml/pref_config_theme.xml
  74. 44
      app/src/main/res/xml/pref_main.xml

@ -1,3 +1,5 @@
## 常见问题
1.为什么第一次安装好之后什么东西都没有? 1.为什么第一次安装好之后什么东西都没有?
* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从QQ群、公众号“开源阅读软件”、酷安评论里获取由书友制作分享的书源。 * 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从QQ群、公众号“开源阅读软件”、酷安评论里获取由书友制作分享的书源。

@ -1,9 +1,20 @@
## 更新日志 ## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
**2020/03/19**
* 美化界面我的 by yangyxd
* 优化搜索
**2020/03/18**
* 尝试修复搜索时崩溃
* 解决看过书籍的移到顶部需要向上滚动才能看到的bug
* 只有再书源被删除找不到书源时才会自动换源
* 美化界面 by yangyxd
* 订阅后台播放
**2020/03/16** **2020/03/16**
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的开源阅读软件迁移过来
* 修复滚动模式切换章节位置不归0的bug * 修复滚动模式切换章节位置不归0的bug
* 修复文字选择更多菜单在部分手机上报错的bug * 修复文字选择更多菜单在部分手机上报错的bug
* 修复文字选择菜单问题 * 修复文字选择菜单问题

@ -32,7 +32,7 @@ data class Book(
var customIntro: String? = null, // 简介内容(用户修改) var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // 0:text 1:audio var type: Int = 0, // 0:text 1:audio
var group: Int = 1, // 自定义分组索引号 var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间 var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间
var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间 var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间

@ -19,7 +19,8 @@ object LauncherIconHelp {
ComponentName(App.INSTANCE, Launcher2::class.java.name), ComponentName(App.INSTANCE, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name), ComponentName(App.INSTANCE, Launcher3::class.java.name),
ComponentName(App.INSTANCE, Launcher4::class.java.name), ComponentName(App.INSTANCE, Launcher4::class.java.name),
ComponentName(App.INSTANCE, Launcher5::class.java.name) ComponentName(App.INSTANCE, Launcher5::class.java.name),
ComponentName(App.INSTANCE, Launcher6::class.java.name)
) )
fun changeIcon(icon: String?) { fun changeIcon(icon: String?) {

@ -6,7 +6,7 @@ import kotlin.coroutines.CoroutineContext
class Coroutine<T>( class Coroutine<T>(
scope: CoroutineScope, val scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO, context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
) { ) {
@ -28,9 +28,10 @@ class Coroutine<T>(
private val job: Job private val job: Job
private var start: VoidCallback? = null private var start: VoidCallback? = null
private var success: Callback<T?>? = null private var success: Callback<T>? = null
private var error: Callback<Throwable>? = null private var error: Callback<Throwable>? = null
private var finally: VoidCallback? = null private var finally: VoidCallback? = null
private var cancel: VoidCallback? = null
private var timeMillis: Long? = null private var timeMillis: Long? = null
private var errorReturn: Result<T>? = null private var errorReturn: Result<T>? = null
@ -45,7 +46,7 @@ class Coroutine<T>(
get() = job.isCompleted get() = job.isCompleted
init { init {
this.job = executeInternal(scope, context, block) this.job = executeInternal(context, block)
} }
fun timeout(timeMillis: () -> Long): Coroutine<T> { fun timeout(timeMillis: () -> Long): Coroutine<T> {
@ -78,7 +79,7 @@ class Coroutine<T>(
fun onSuccess( fun onSuccess(
context: CoroutineContext? = null, context: CoroutineContext? = null,
block: suspend CoroutineScope.(T?) -> Unit block: suspend CoroutineScope.(T) -> Unit
): Coroutine<T> { ): Coroutine<T> {
this.success = Callback(context, block) this.success = Callback(context, block)
return this@Coroutine return this@Coroutine
@ -100,9 +101,28 @@ class Coroutine<T>(
return this@Coroutine return this@Coroutine
} }
fun onCancel(
context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit
): Coroutine<T> {
this.cancel = VoidCallback(context, block)
return this@Coroutine
}
//取消当前任务 //取消当前任务
fun cancel(cause: CancellationException? = null) { fun cancel(cause: CancellationException? = null) {
job.cancel(cause) job.cancel(cause)
cancel?.let {
MainScope().launch {
if (null == it.context) {
it.block.invoke(scope)
} else {
withContext(scope.coroutineContext.plus(it.context)) {
it.block.invoke(this)
}
}
}
}
} }
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {
@ -110,7 +130,6 @@ class Coroutine<T>(
} }
private fun executeInternal( private fun executeInternal(
scope: CoroutineScope,
context: CoroutineContext, context: CoroutineContext,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
): Job { ): Job {
@ -118,24 +137,30 @@ class Coroutine<T>(
try { try {
start?.let { dispatchVoidCallback(this, it) } start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, context, timeMillis ?: 0L, block) val value = executeBlock(scope, context, timeMillis ?: 0L, block)
if (isActive) {
success?.let { dispatchCallback(this, value, it) } success?.let { dispatchCallback(this, value, it) }
}
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
val consume: Boolean = errorReturn?.value?.let { value -> val consume: Boolean = errorReturn?.value?.let { value ->
if (isActive) {
success?.let { dispatchCallback(this, value, it) } success?.let { dispatchCallback(this, value, it) }
}
true true
} ?: false } ?: false
if (!consume) { if (!consume && isActive) {
error?.let { dispatchCallback(this, e, it) } error?.let { dispatchCallback(this, e, it) }
} }
} finally { } finally {
if (isActive) {
finally?.let { dispatchVoidCallback(this, it) } finally?.let { dispatchVoidCallback(this, it) }
} }
} }
} }
}
private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) { private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) {
if (null == callback.context) { if (null == callback.context) {
@ -152,6 +177,7 @@ class Coroutine<T>(
value: R, value: R,
callback: Callback<R> callback: Callback<R>
) { ) {
if (!scope.isActive) return
if (null == callback.context) { if (null == callback.context) {
callback.block.invoke(scope, value) callback.block.invoke(scope, value)
} else { } else {
@ -166,7 +192,7 @@ class Coroutine<T>(
context: CoroutineContext, context: CoroutineContext,
timeMillis: Long, timeMillis: Long,
noinline block: suspend CoroutineScope.() -> T noinline block: suspend CoroutineScope.() -> T
): T? { ): T {
return withContext(scope.coroutineContext.plus(context)) { return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) { if (timeMillis > 0L) withTimeout(timeMillis) {
block() block()

@ -54,6 +54,7 @@ object Backup {
suspend fun backup(context: Context, path: String = legadoPath) { suspend fun backup(context: Context, path: String = legadoPath) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
synchronized(this@Backup) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath) writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath) writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath) writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
@ -86,6 +87,7 @@ object Backup {
} }
} }
} }
}
private fun writeListToJson(list: List<Any>, fileName: String, path: String) { private fun writeListToJson(list: List<Any>, fileName: String, path: String) {
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
@ -96,6 +98,7 @@ object Backup {
@Throws(java.lang.Exception::class) @Throws(java.lang.Exception::class)
private fun copyBackup(context: Context, uri: Uri) { private fun copyBackup(context: Context, uri: Uri) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)

@ -40,7 +40,6 @@ object OldBook {
book.durChapterTitle = jsonItem.readString("$.durChapterName") book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce") book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName") book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0

@ -56,9 +56,7 @@ object Debug {
log(debugSource, "︾开始解析") log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null) Rss.getArticles(rssSource, null)
.onSuccess { .onSuccess {
if (it == null) { if (it.articles.isEmpty()) {
log(debugSource, "︽解析失败", state = -1)
} else if(it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空") log(debugSource, "⇒列表页解析成功,为空")
log(debugSource, "︽解析完成", state = 1000) log(debugSource, "︽解析完成", state = 1000)
} else { } else {
@ -140,7 +138,6 @@ object Debug {
log(debugSource, "︾开始解析发现页") log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1) val explore = webBook.exploreBook(url, 1)
.onSuccess { exploreBooks -> .onSuccess { exploreBooks ->
exploreBooks?.let {
if (exploreBooks.isNotEmpty()) { if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成") log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
@ -149,7 +146,6 @@ object Debug {
log(debugSource, "︽未获取到书籍", state = -1) log(debugSource, "︽未获取到书籍", state = -1)
} }
} }
}
.onError { .onError {
log( log(
debugSource, debugSource,
@ -164,7 +160,6 @@ object Debug {
log(debugSource, "︾开始解析搜索页") log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1) val search = webBook.searchBook(key, 1)
.onSuccess { searchBooks -> .onSuccess { searchBooks ->
searchBooks?.let {
if (searchBooks.isNotEmpty()) { if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成") log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
@ -173,7 +168,6 @@ object Debug {
log(debugSource, "︽未获取到书籍", state = -1) log(debugSource, "︽未获取到书籍", state = -1)
} }
} }
}
.onError { .onError {
log(debugSource, it.localizedMessage, state = -1) log(debugSource, it.localizedMessage, state = -1)
} }
@ -197,8 +191,7 @@ object Debug {
private fun tocDebug(webBook: WebBook, book: Book) { private fun tocDebug(webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页") log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book) val chapterList = webBook.getChapterList(book)
.onSuccess { chapterList -> .onSuccess {
chapterList?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成") log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
@ -208,7 +201,6 @@ object Debug {
log(debugSource, "︽目录列表为空", state = -1) log(debugSource, "︽目录列表为空", state = -1)
} }
} }
}
.onError { .onError {
log(debugSource, it.localizedMessage, state = -1) log(debugSource, it.localizedMessage, state = -1)
} }

@ -0,0 +1,93 @@
package io.legado.app.model
import io.legado.app.App
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var mSearchId = System.currentTimeMillis()
private var searchPage = 1
private var searchKey: String = ""
private var task: Coroutine<*>? = null
private fun initSearchPool() {
searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
}
fun search(searchId: Long, key: String) {
if (searchId != mSearchId) {
task?.cancel()
searchPool.close()
initSearchPool()
mSearchId = searchId
searchPage = 1
if (key.isEmpty()) {
return
} else {
this.searchKey = key
}
} else {
searchPage++
}
task = Coroutine.async(scope, searchPool) {
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
if (searchId == mSearchId) {
callBack.onSearchSuccess(it)
}
}
}
}.onStart {
callBack.onSearchStart()
}
task?.invokeOnCompletion {
if (searchId == mSearchId) {
callBack.onSearchFinish()
}
}
}
fun cancelSearch() {
task?.cancel()
mSearchId = 0
callBack.onSearchCancel()
}
fun close() {
task?.cancel()
mSearchId = 0
searchPool.close()
}
interface CallBack {
fun onSearchStart()
fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
fun onSearchFinish()
fun onSearchCancel()
}
}

@ -27,13 +27,14 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1, page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> { ): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
searchBookSuspend(key, page) searchBookSuspend(scope, key, page)
} }
} }
suspend fun searchBookSuspend( suspend fun searchBookSuspend(
scope: CoroutineScope,
key: String, key: String,
page: Int? = 1 page: Int? = 1
): ArrayList<SearchBook> { ): ArrayList<SearchBook> {
@ -47,6 +48,7 @@ class WebBook(val bookSource: BookSource) {
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
return BookList.analyzeBookList( return BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,
@ -75,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList( BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,

@ -9,11 +9,15 @@ import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
object BookList { object BookList {
@Throws(Exception::class) @Throws(Exception::class)
fun analyzeBookList( fun analyzeBookList(
scope: CoroutineScope,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
analyzeUrl: AnalyzeUrl, analyzeUrl: AnalyzeUrl,
@ -28,12 +32,13 @@ object BookList {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(null) val analyzeRule = AnalyzeRule(null)
analyzeRule.setContent(body, baseUrl) analyzeRule.setContent(body, baseUrl)
bookSource.bookUrlPattern?.let { bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) { if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -59,7 +64,7 @@ object BookList {
collections = analyzeRule.getElements(ruleList) collections = analyzeRule.getElements(ruleList)
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -74,8 +79,9 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) { for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem( getSearchItem(
item, analyzeRule, bookSource, baseUrl, index == 0, scope, item, analyzeRule, bookSource, baseUrl, index == 0,
ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor, ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,
ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind, ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,
ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount
@ -93,7 +99,9 @@ object BookList {
return bookList return bookList
} }
@Throws(Exception::class)
private fun getInfoItem( private fun getInfoItem(
scope: CoroutineScope,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String baseUrl: String
@ -108,29 +116,37 @@ object BookList {
with(bookSource.getBookInfoRule()) { with(bookSource.getBookInfoRule()) {
init?.let { init?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it)) analyzeRule.setContent(analyzeRule.getElement(it))
} }
} }
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名") Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = analyzeRule.getString(name) searchBook.name = analyzeRule.getString(name)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}") Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}") Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount) searchBook.wordCount = analyzeRule.getString(wordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}") Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter) searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}") Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro) searchBook.intro = analyzeRule.getString(intro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true) searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -140,7 +156,9 @@ object BookList {
return null return null
} }
@Throws(Exception::class)
private fun getSearchItem( private fun getSearchItem(
scope: CoroutineScope,
item: Any, item: Any,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
@ -162,30 +180,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook analyzeRule.book = searchBook
analyzeRule.setContent(item) analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = analyzeRule.getString(ruleName) searchBook.name = analyzeRule.getString(ruleName)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount) searchBook.wordCount = analyzeRule.getString(ruleWordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro) searchBook.intro = analyzeRule.getString(ruleIntro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let { analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true) searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) { if (searchBook.bookUrl.isEmpty()) {

@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -275,7 +275,7 @@ class AudioPlayService : BaseService(),
AudioPlay.book?.let { book -> AudioPlay.book?.let { book ->
AudioPlay.webBook?.getContent(book, chapter, scope = this) AudioPlay.webBook?.getContent(book, chapter, scope = this)
?.onSuccess(IO) { content -> ?.onSuccess(IO) { content ->
if (content.isNullOrEmpty()) { if (content.isEmpty()) {
withContext(Main) { withContext(Main) {
toast("未获取到资源链接") toast("未获取到资源链接")
} }

@ -130,11 +130,9 @@ class DownloadService : BaseService() {
// notificationContent = "启动:" + chapter.title // notificationContent = "启动:" + chapter.title
//} //}
.onSuccess(IO) { content -> .onSuccess(IO) { content ->
content?.let {
downloadCount[entry.key]?.increaseSuccess() downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content) BookHelp.saveContent(book, chapter, content)
} }
}
.onFinally(IO) { .onFinally(IO) {
synchronized(this@DownloadService) { synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished() downloadCount[entry.key]?.increaseFinished()

@ -64,6 +64,11 @@ object ReadBook {
} }
} }
fun upMsg(msg: String?) {
this.msg = msg
callBack?.upContent()
}
fun moveToNextPage() { fun moveToNextPage() {
durPageIndex++ durPageIndex++
callBack?.upContent() callBack?.upContent()
@ -240,7 +245,7 @@ object ReadBook {
book?.let { book -> book?.let { book ->
webBook?.getContent(book, chapter) webBook?.getContent(book, chapter)
?.onSuccess(Dispatchers.IO) { content -> ?.onSuccess(Dispatchers.IO) { content ->
if (content.isNullOrEmpty()) { if (content.isEmpty()) {
contentLoadFinish( contentLoadFinish(
chapter, chapter,
App.INSTANCE.getString(R.string.content_empty), App.INSTANCE.getString(R.string.content_empty),

@ -72,7 +72,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
execute { execute {
AudioPlay.webBook?.getChapterList(book, this) AudioPlay.webBook?.getChapterList(book, this)
?.onSuccess(Dispatchers.IO) { cList -> ?.onSuccess(Dispatchers.IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
AudioPlay.chapterSize = cList.size AudioPlay.chapterSize = cList.size

@ -51,14 +51,13 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
fun search() { fun search() {
task = execute { task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(Dispatchers.IO) { .onSuccess(Dispatchers.IO) {
if (it != null && it.isNotEmpty()) { if (it.isNotEmpty()) {
val searchBook = it[0] val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty() && !searchBook.coverUrl.isNullOrEmpty()
@ -72,6 +71,10 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
} }
} }
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {

@ -72,14 +72,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
fun search() { fun search() {
task = execute { task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { .onSuccess(IO) {
it?.forEach { searchBook -> it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) { if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) { if (searchBook.tocUrl.isEmpty()) {
@ -95,6 +94,10 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
} }
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {
@ -107,7 +110,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess { .onSuccess {
it?.let { loadChapter(it) } loadChapter(it)
}.onError { }.onError {
debug { context.getString(R.string.error_get_book_info) } debug { context.getString(R.string.error_get_book_info) }
} }
@ -119,14 +122,12 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute { execute {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) { chapters ->
it?.let { chapters ->
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook() val searchBook: SearchBook = book.toSearchBook()
searchFinish(searchBook) searchFinish(searchBook)
} }
}
}.onError { }.onError {
debug { context.getString(R.string.error_get_chapter_list) } debug { context.getString(R.string.error_get_chapter_list) }
} }

@ -35,13 +35,11 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
WebBook(source).exploreBook(url, page, this) WebBook(source).exploreBook(url, page, this)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { searchBooks -> .onSuccess(IO) { searchBooks ->
searchBooks?.let {
booksData.postValue(searchBooks) booksData.postValue(searchBooks)
App.db.searchBookDao().insert(*searchBooks.toTypedArray()) App.db.searchBookDao().insert(*searchBooks.toTypedArray())
page++ page++
} }
} }
} }
}
} }

@ -59,13 +59,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let {
bookData.postValue(book) bookData.postValue(book)
if (inBookshelf) { if (inBookshelf) {
App.db.bookDao().update(book) App.db.bookDao().update(book)
} }
loadChapter(it, changeDruChapterIndex) loadChapter(it, changeDruChapterIndex)
}
}.onError { }.onError {
toast(R.string.error_get_book_info) toast(R.string.error_get_book_info)
} }
@ -92,7 +90,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (inBookshelf) { if (inBookshelf) {
App.db.bookDao().update(book) App.db.bookDao().update(book)
@ -106,7 +103,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} else { } else {
toast(R.string.chapter_list_empty) toast(R.string.chapter_list_empty)
} }
}
}.onError { }.onError {
chapterListData.postValue(null) chapterListData.postValue(null)
toast(R.string.error_get_chapter_list) toast(R.string.error_get_chapter_list)

@ -92,6 +92,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override val headerHeight: Int get() = page_view.curPage.headerHeight override val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ReadBook.msg = null
Help.setOrientation(this) Help.setOrientation(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
@ -682,6 +683,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss() textActionMenu?.dismiss()
page_view.onDestroy() page_view.onDestroy()
ReadBook.msg = null
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
SyncBookProgress.uploadBookProgress() SyncBookProgress.uploadBookProgress()
Backup.autoBack(this) Backup.autoBack(this)

@ -112,20 +112,21 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else { } else {
ReadBook.webBook?.getChapterList(book, this) ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList -> ?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book) App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size ReadBook.chapterSize = cList.size
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true) ReadBook.loadContent(resetPageOffset = true)
} else { } else {
changeDruChapterIndex(cList) changeDruChapterIndex(cList)
} }
} else { } else {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} }
}?.onError { }?.onError {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} }
} }
} }
@ -133,6 +134,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
ReadBook.upMsg(null)
ReadBook.book?.let { ReadBook.book?.let {
App.db.bookDao().delete(it) App.db.bookDao().delete(it)
} }
@ -159,7 +161,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
execute { execute {
App.db.bookSourceDao().allTextEnabled.forEach { source -> App.db.bookSourceDao().allTextEnabled.forEach { source ->
try { try {
val searchBooks = WebBook(source).searchBookSuspend(name) val searchBooks = WebBook(source).searchBookSuspend(this, name)
searchBooks.getOrNull(0)?.let { searchBooks.getOrNull(0)?.let {
if (it.name == name && (it.author == author || author == "")) { if (it.name == name && (it.author == author || author == "")) {
changeTo(it.toBook()) changeTo(it.toBook())
@ -171,11 +173,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
}.onStart { }.onStart {
ReadBook.msg = "正在自动换源" ReadBook.upMsg("正在自动换源")
ReadBook.callBack?.upContent()
}.onFinally { }.onFinally {
ReadBook.msg = null ReadBook.upMsg(null)
ReadBook.callBack?.upContent()
} }
} }

@ -49,9 +49,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle() val payload = Bundle()
val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
payload.putString("name", newItem.name) payload.putString("name", newItem.name)
} }
@ -73,6 +73,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.intro != newItem.intro) { if (oldItem.intro != newItem.intro) {
payload.putString("intro", newItem.intro) payload.putString("intro", newItem.intro)
} }
if (payload.isEmpty) {
return null
}
return payload return payload
} }
} }

@ -112,6 +112,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
viewModel.saveSearchKey(query) viewModel.saveSearchKey(query)
viewModel.search(it) viewModel.search(it)
} }
openOrCloseHistory(false)
return true return true
} }
@ -128,6 +129,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
openOrCloseHistory(hasFocus) openOrCloseHistory(hasFocus)
} }
} }
openOrCloseHistory(true)
} }
private fun initRecyclerView() { private fun initRecyclerView() {

@ -7,82 +7,62 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig import io.legado.app.model.SearchBookModel
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.isActive
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application)
private var searchPool = , SearchBookModel.CallBack {
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() private val searchBookModel = SearchBookModel(this, this)
private var task: Coroutine<*>? = null
var isSearchLiveData = MutableLiveData<Boolean>() var isSearchLiveData = MutableLiveData<Boolean>()
var searchBookLiveData = MutableLiveData<List<SearchBook>>() var searchBookLiveData = MutableLiveData<List<SearchBook>>()
var searchKey: String = "" var searchKey: String = ""
var searchPage = 1
var isLoading = false var isLoading = false
var searchBooks = arrayListOf<SearchBook>() var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L
/** /**
* 开始搜索 * 开始搜索
*/ */
fun search(key: String) { fun search(key: String) {
task?.cancel() if (searchKey != key && key.isNotEmpty()) {
if (key.isEmpty() && searchKey.isEmpty()) { searchBookModel.cancelSearch()
return
} else if (key.isEmpty()) {
isLoading = true
searchPage++
} else if (key.isNotEmpty()) {
isLoading = true
searchPage = 1
searchKey = key
searchBooks.clear() searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
} }
searchBookModel.search(searchID, searchKey)
}
override fun onSearchStart() {
isSearchLiveData.postValue(true) isSearchLiveData.postValue(true)
task = execute { }
val searchGroup = context.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) { override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this@execute,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
it?.let { list ->
if (context.getPrefBoolean(PreferKey.precisionSearch)) { if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(list) precisionSearch(this, searchBooks)
} else { } else {
App.db.searchBookDao().insert(*list.toTypedArray()) App.db.searchBookDao().insert(*searchBooks.toTypedArray())
mergeItems(list) mergeItems(this, searchBooks)
}
}
}
} }
} }
task?.invokeOnCompletion { override fun onSearchFinish() {
isSearchLiveData.postValue(false) isSearchLiveData.postValue(false)
isLoading = false isLoading = false
} }
override fun onSearchCancel() {
isSearchLiveData.postValue(false)
isLoading = false
} }
/** /**
* 精确搜索处理 * 精确搜索处理
*/ */
private fun precisionSearch(searchBooks: List<SearchBook>) { private fun precisionSearch(scope: CoroutineScope, searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>() val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook -> searchBooks.forEach { searchBook ->
if (searchBook.name.contains(searchKey, true) if (searchBook.name.contains(searchKey, true)
@ -90,14 +70,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
) books.add(searchBook) ) books.add(searchBook)
} }
App.db.searchBookDao().insert(*books.toTypedArray()) App.db.searchBookDao().insert(*books.toTypedArray())
mergeItems(books) if (scope.isActive) {
mergeItems(scope, books)
}
} }
/** /**
* 合并搜索结果并排序 * 合并搜索结果并排序
*/ */
@Synchronized @Synchronized
private fun mergeItems(newDataS: List<SearchBook>) { private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>) {
if (newDataS.isNotEmpty()) { if (newDataS.isNotEmpty()) {
val copyDataS = ArrayList(searchBooks) val copyDataS = ArrayList(searchBooks)
val searchBooksAdd = ArrayList<SearchBook>() val searchBooksAdd = ArrayList<SearchBook>()
@ -141,6 +123,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
if (!scope.isActive) return
searchBooks.sortWith(Comparator { o1, o2 -> searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) { if (o1.name == searchKey && o2.name != searchKey) {
1 1
@ -166,6 +149,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
0 0
} }
}) })
if (!scope.isActive) return
searchBooks = copyDataS searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS) searchBookLiveData.postValue(copyDataS)
} }
@ -175,7 +159,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
* 停止搜索 * 停止搜索
*/ */
fun stop() { fun stop() {
task?.cancel() searchBookModel.cancelSearch()
} }
/** /**
@ -211,7 +195,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchPool.close() searchBookModel.close()
} }
} }

@ -211,7 +211,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}.onError { }.onError {
finally(it.localizedMessage ?: "") finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it ?: "导入完成") finally(it)
} }
} }

@ -33,12 +33,10 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
updateList.remove(book.bookUrl) updateList.remove(book.bookUrl)
postEvent(EventBus.UP_BOOK, book.bookUrl) postEvent(EventBus.UP_BOOK, book.bookUrl)
} }
it?.let {
App.db.bookDao().update(book) App.db.bookDao().update(book)
App.db.bookChapterDao().delByBook(book.bookUrl) App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray()) App.db.bookChapterDao().insert(*it.toTypedArray())
} }
}
.onError { .onError {
synchronized(this) { synchronized(this) {
updateList.remove(book.bookUrl) updateList.remove(book.bookUrl)

@ -43,10 +43,8 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
) )
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let { book -> App.db.bookDao().insert(it)
App.db.bookDao().insert(book)
successCount++ successCount++
}
}.onError { }.onError {
throw Exception(it.localizedMessage) throw Exception(it.localizedMessage)
} }

@ -24,7 +24,7 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) : class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) :
SimpleRecyclerAdapter<BookSource>(context, R.layout.item_find_book) { SimpleRecyclerAdapter<BookSource>(context, R.layout.item_find_book) {
private var exIndex = 0 private var exIndex = -1
private var scrollTo = -1 private var scrollTo = -1
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {

@ -44,8 +44,8 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
rssSource?.let { rssSource -> rssSource?.let { rssSource ->
Rss.getArticles(rssSource, null) Rss.getArticles(rssSource, null)
.onSuccess(IO) { .onSuccess(IO) {
nextPageUrl = it?.nextPageUrl nextPageUrl = it.nextPageUrl
it?.articles?.let { list -> it.articles.let { list ->
list.forEach { rssArticle -> list.forEach { rssArticle ->
rssArticle.order = order-- rssArticle.order = order--
} }
@ -76,8 +76,8 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
if (source != null && !pageUrl.isNullOrEmpty()) { if (source != null && !pageUrl.isNullOrEmpty()) {
Rss.getArticles(source, pageUrl) Rss.getArticles(source, pageUrl)
.onSuccess(IO) { .onSuccess(IO) {
nextPageUrl = it?.nextPageUrl nextPageUrl = it.nextPageUrl
it?.articles?.let { list -> it.articles.let { list ->
if (list.isEmpty()) { if (list.isEmpty()) {
callBack?.loadFinally(false) callBack?.loadFinally(false)
return@let return@let

@ -0,0 +1,17 @@
package io.legado.app.ui.rss.read
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
class VisibleWebView(
context: Context,
attrs: AttributeSet? = null
) : WebView(context, attrs) {
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(View.VISIBLE)
}
}

@ -187,7 +187,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
}.onError { }.onError {
finally(it.localizedMessage ?: "") finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it ?: "导入完成") finally(it)
} }
} }

@ -0,0 +1,110 @@
package io.legado.app.ui.widget
import android.app.SearchableInfo
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import io.legado.app.R
class SearchView : SearchView {
private var mSearchHintIcon: Drawable? = null
private var textView: TextView? = null
constructor(
context: Context?,
attrs: AttributeSet? = null
) : super(context, attrs)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
try {
if (textView == null) {
textView = findViewById(androidx.appcompat.R.id.search_src_text)
mSearchHintIcon = this.context.getDrawable(R.drawable.ic_search_hint)
updateQueryHint()
}
// 改变字体
textView!!.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
textView!!.gravity = Gravity.CENTER_VERTICAL
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getDecoratedHint(hintText: CharSequence): CharSequence {
// If the field is always expanded or we don't have a search hint icon,
// then don't add the search icon to the hint.
if (mSearchHintIcon == null) {
return hintText
}
val textSize = (textView!!.textSize * 0.8).toInt()
mSearchHintIcon!!.setBounds(0, 0, textSize, textSize)
val ssb = SpannableStringBuilder(" ")
ssb.setSpan(CenteredImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
ssb.append(hintText)
return ssb
}
private fun updateQueryHint() {
textView?.let {
it.hint = getDecoratedHint(queryHint ?: "")
}
}
override fun setIconifiedByDefault(iconified: Boolean) {
super.setIconifiedByDefault(iconified)
updateQueryHint()
}
override fun setSearchableInfo(searchable: SearchableInfo?) {
super.setSearchableInfo(searchable)
searchable?.let {
updateQueryHint()
}
}
override fun setQueryHint(hint: CharSequence?) {
super.setQueryHint(hint)
updateQueryHint()
}
internal class CenteredImageSpan(drawable: Drawable?) : ImageSpan(drawable!!) {
override fun draw(
canvas: Canvas, text: CharSequence,
start: Int, end: Int, x: Float,
top: Int, y: Int, bottom: Int, paint: Paint
) {
// image to draw
val b = drawable
// font metrics of text to be replaced
val fm = paint.fontMetricsInt
val transY = ((y + fm.descent + y + fm.ascent) / 2
- b.bounds.bottom / 2)
canvas.save()
canvas.translate(x, transY.toFloat())
b.draw(canvas)
canvas.restore()
}
}
}

@ -0,0 +1,185 @@
package io.legado.app.ui.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import io.legado.app.R
import io.legado.app.utils.getCompatColor
/**
* ShadowLayout.java
*
*
* Created by lijiankun on 17/8/11.
*/
class ShadowLayout(
context: Context,
attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val mPaint =
Paint(Paint.ANTI_ALIAS_FLAG)
private val mRectF = RectF()
/**
* 阴影的颜色
*/
private var mShadowColor = Color.TRANSPARENT
/**
* 阴影的大小范围
*/
private var mShadowRadius = 0f
/**
* 阴影 x 轴的偏移量
*/
private var mShadowDx = 0f
/**
* 阴影 y 轴的偏移量
*/
private var mShadowDy = 0f
/**
* 阴影显示的边界
*/
private var mShadowSide = ALL
/**
* 阴影的形状圆形/矩形
*/
private var mShadowShape = SHAPE_RECTANGLE
init {
setLayerType(View.LAYER_TYPE_SOFTWARE, null) // 关闭硬件加速
setWillNotDraw(false) // 调用此方法后,才会执行 onDraw(Canvas) 方法
val typedArray =
context.obtainStyledAttributes(attrs, R.styleable.ShadowLayout)
mShadowColor = typedArray.getColor(
R.styleable.ShadowLayout_shadowColor,
context.getCompatColor(android.R.color.black)
)
mShadowRadius =
typedArray.getDimension(R.styleable.ShadowLayout_shadowRadius, dip2px(0f))
mShadowDx = typedArray.getDimension(R.styleable.ShadowLayout_shadowDx, dip2px(0f))
mShadowDy = typedArray.getDimension(R.styleable.ShadowLayout_shadowDy, dip2px(0f))
mShadowSide =
typedArray.getInt(R.styleable.ShadowLayout_shadowSide, ALL)
mShadowShape = typedArray.getInt(
R.styleable.ShadowLayout_shadowShape,
SHAPE_RECTANGLE
)
typedArray.recycle()
setUpShadowPaint()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val effect = mShadowRadius + dip2px(5f)
var rectLeft = 0f
var rectTop = 0f
var rectRight = this.measuredWidth.toFloat()
var rectBottom = this.measuredHeight.toFloat()
var paddingLeft = 0
var paddingTop = 0
var paddingRight = 0
var paddingBottom = 0
this.width
if (mShadowSide and LEFT == LEFT) {
rectLeft = effect
paddingLeft = effect.toInt()
}
if (mShadowSide and TOP == TOP) {
rectTop = effect
paddingTop = effect.toInt()
}
if (mShadowSide and RIGHT == RIGHT) {
rectRight = this.measuredWidth - effect
paddingRight = effect.toInt()
}
if (mShadowSide and BOTTOM == BOTTOM) {
rectBottom = this.measuredHeight - effect
paddingBottom = effect.toInt()
}
if (mShadowDy != 0.0f) {
rectBottom -= mShadowDy
paddingBottom += mShadowDy.toInt()
}
if (mShadowDx != 0.0f) {
rectRight -= mShadowDx
paddingRight += mShadowDx.toInt()
}
mRectF.left = rectLeft
mRectF.top = rectTop
mRectF.right = rectRight
mRectF.bottom = rectBottom
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
}
/**
* 真正绘制阴影的方法
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
setUpShadowPaint()
if (mShadowShape == SHAPE_RECTANGLE) {
canvas.drawRect(mRectF, mPaint)
} else if (mShadowShape == SHAPE_OVAL) {
canvas.drawCircle(
mRectF.centerX(),
mRectF.centerY(),
mRectF.width().coerceAtMost(mRectF.height()) / 2,
mPaint
)
}
}
fun setShadowColor(shadowColor: Int) {
mShadowColor = shadowColor
requestLayout()
postInvalidate()
}
fun setShadowRadius(shadowRadius: Float) {
mShadowRadius = shadowRadius
requestLayout()
postInvalidate()
}
private fun setUpShadowPaint() {
mPaint.reset()
mPaint.isAntiAlias = true
mPaint.color = Color.TRANSPARENT
mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor)
}
/**
* dip2px dp 值转 px
*
* @param dpValue dp
* @return px
*/
private fun dip2px(dpValue: Float): Float {
val dm = context.resources.displayMetrics
val scale = dm.density
return dpValue * scale + 0.5f
}
companion object {
const val ALL = 0x1111
const val LEFT = 0x0001
const val TOP = 0x0010
const val RIGHT = 0x0100
const val BOTTOM = 0x1000
const val SHAPE_RECTANGLE = 0x0001
const val SHAPE_OVAL = 0x0010
}
}

@ -37,6 +37,8 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
init { init {
isPersistent = true isPersistent = true
layoutResource = io.legado.app.R.layout.view_preference
val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference) val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference)
showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true) showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true)
@ -109,9 +111,12 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
} }
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
val v = io.legado.app.ui.widget.prefs.Preference.bindView<ColorPanelView>(context, holder, icon, title, summary, widgetLayoutResource,
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30)
if (v is ColorPanelView) {
v.color = color
}
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
val preview = holder.itemView.findViewById(R.id.cpv_preference_preview_color_panel) as ColorPanelView
preview.color = color
} }
override fun onSetInitialValue(defaultValue: Any?) { override fun onSetInitialValue(defaultValue: Any?) {

@ -0,0 +1,21 @@
package io.legado.app.ui.widget.prefs
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import io.legado.app.R
class EditTextPreference(context: Context, attrs: AttributeSet) : androidx.preference.EditTextPreference(context, attrs) {
init {
// isPersistent = true
layoutResource = R.layout.view_preference
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
super.onBindViewHolder(holder)
}
}

@ -29,6 +29,7 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
private val mEntryDrawables = arrayListOf<Drawable?>() private val mEntryDrawables = arrayListOf<Drawable?>()
init { init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.view_icon widgetLayoutResource = R.layout.view_icon
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0)
@ -52,11 +53,12 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
override fun onBindViewHolder(holder: PreferenceViewHolder?) { override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
holder?.itemView?.findViewById<ImageView>(R.id.preview)?.let { val v = Preference.bindView<ImageView>(context, holder, icon, title, summary, widgetLayoutResource, R.id.preview, 50, 50)
if (v is ImageView) {
val selectedIndex = findIndexOfValue(value) val selectedIndex = findIndexOfValue(value)
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
val drawable = mEntryDrawables[selectedIndex] val drawable = mEntryDrawables[selectedIndex]
it.setImageDrawable(drawable) v.setImageDrawable(drawable)
} }
} }
} }

@ -11,12 +11,15 @@ import io.legado.app.R
class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) {
init { init {
layoutResource = R.layout.view_preference
widgetLayoutResource = R.layout.item_fillet_text widgetLayoutResource = R.layout.item_fillet_text
} }
override fun onBindViewHolder(holder: PreferenceViewHolder?) { override fun onBindViewHolder(holder: PreferenceViewHolder?) {
val v = Preference.bindView<TextView>(context, holder, icon, title, summary, widgetLayoutResource, R.id.text_view)
if (v is TextView) {
v.text = entry
}
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
val textView = holder?.itemView?.findViewById<TextView>(R.id.text_view)
textView?.text = entry
} }
} }

@ -0,0 +1,85 @@
package io.legado.app.ui.widget.prefs
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceViewHolder
import io.legado.app.R
import io.legado.app.lib.theme.accentColor
import org.jetbrains.anko.layoutInflater
import kotlin.math.roundToInt
class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Preference(context, attrs) {
init {
// isPersistent = true
layoutResource = R.layout.view_preference
}
companion object {
fun <T: View> bindView(context: Context, it: PreferenceViewHolder?, icon: Drawable?, title: CharSequence?, summary: CharSequence?, weightLayoutRes: Int?, viewId: Int?,
weightWidth: Int = 0, weightHeight: Int = 0): T? {
if (it == null) return null
val view = it.findViewById(R.id.preference_title)
if (view is TextView) {
view.text = title
view.isVisible = title != null && title.isNotEmpty()
val tvSummary = it.findViewById(R.id.preference_desc)
if (tvSummary is TextView) {
tvSummary.text = summary
tvSummary.isVisible = summary != null && summary.isNotEmpty()
}
val iconView = it.findViewById(R.id.preference_icon)
if (iconView is ImageView) {
iconView.isVisible = icon != null && icon.isVisible
iconView.setImageDrawable(icon)
iconView.setColorFilter(context.accentColor)
}
}
if (weightLayoutRes != null && weightLayoutRes != 0 && viewId != null && viewId != 0) {
val lay = it.findViewById(R.id.preference_widget)
if (lay is FrameLayout) {
var v = it.itemView.findViewById<T>(viewId)
if (v == null) {
val inflater: LayoutInflater = context.layoutInflater
val childView = inflater.inflate(weightLayoutRes, null)
lay.removeAllViews()
lay.addView(childView)
lay.isVisible = true
v = lay.findViewById(viewId)
}
if (weightWidth > 0 || weightHeight > 0) {
val lp = lay.layoutParams
if (weightHeight > 0)
lp.height = (context.resources.displayMetrics.density * weightHeight).roundToInt()
if (weightWidth > 0)
lp.width = (context.resources.displayMetrics.density * weightWidth).roundToInt()
lay.layoutParams = lp
}
return v
}
}
return null
}
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
bindView<View>(context, holder, icon, title, summary, null, null)
super.onBindViewHolder(holder)
}
}

@ -1,22 +1,41 @@
package io.legado.app.ui.widget.prefs package io.legado.app.ui.widget.prefs
import android.content.Context import android.content.Context
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import io.legado.app.R
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
class PreferenceCategory(context: Context, attrs: AttributeSet) : class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) {
PreferenceCategory(context, attrs) {
init {
isPersistent = true
layoutResource = R.layout.view_preference_category
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) { override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
holder?.let { holder?.let {
val view = it.findViewById(android.R.id.title) val view = it.findViewById(R.id.preference_title)
if (view is TextView && !view.isInEditMode) { if (view is TextView) { // && !view.isInEditMode
view.text = title
view.setTextColor(context.accentColor) //设置title文本的颜色 view.setTextColor(context.accentColor) //设置title文本的颜色
view.isVisible = title != null && title.isNotEmpty()
val da = it.findViewById(R.id.preference_divider_above)
if (da is View) {
da.isVisible = it.isDividerAllowedAbove
}
val db = it.findViewById(R.id.preference_divider_below)
if (db is View) {
db.isVisible = it.isDividerAllowedBelow
}
} }
} }
} }

@ -12,14 +12,24 @@ import io.legado.app.lib.theme.accentColor
class SwitchPreference(context: Context, attrs: AttributeSet) : class SwitchPreference(context: Context, attrs: AttributeSet) :
SwitchPreferenceCompat(context, attrs) { SwitchPreferenceCompat(context, attrs) {
override fun onBindViewHolder(holder: PreferenceViewHolder?) { init {
super.onBindViewHolder(holder) layoutResource = R.layout.view_preference
holder?.let {
val view = it.findViewById(R.id.switchWidget)
if (view is SwitchCompat) {
ATH.setTint(view, context.accentColor)
} }
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
val v = Preference.bindView<SwitchCompat>(
context,
holder,
icon,
title,
summary,
widgetLayoutResource,
R.id.switchWidget
)
if (v is SwitchCompat) {
ATH.setTint(v, context.accentColor)
} }
super.onBindViewHolder(holder)
} }
} }

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/btn_bg_press">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/background_prefs" />
<corners android:radius="0dp" />
</shape>
</item>
</ripple>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5A7.5,7.5 0,1 1,4.5 12,7.5 7.5,0 0,1 12,4.5M12,3a9,9 0,1 0,9 9,9 9,0 0,0 -9,-9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M11.25,10.5h1.5v6h-1.5z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.94,4H5A2,2 0,0 0,3 6V18a2,2 0,0 0,2 2H19a2,2 0,0 0,2 -2V8a2,2 0,0 0,-2 -2H13.23a1.49,1.49 0,0 1,-1 -0.42L11,4.42a1.49,1.49 0,0 0,-1 -0.42ZM4.5,9.25V6A0.5,0.5 0,0 1,5 5.5H9.94l1.21,1.16a3,3 0,0 0,2.08 0.84H19a0.5,0.5 0,0 1,0.5 0.5V9.25ZM5,18.5a0.5,0.5 0,0 1,-0.5 -0.5V10.75h15V18a0.5,0.5 0,0 1,-0.5 0.5Z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="32dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,4.5A7.5,7.5 0,1 1,4.5 12,7.5 7.5,0 0,1 12,4.5M12,3a9,9 0,1 0,9 9,9 9,0 0,0 -9,-9Z"/>
<path android:fillColor="#FF000000" android:pathData="M12,5.25v13.5a6.75,6.75 0,0 0,0 -13.5Z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5A7.5,7.5 0,1 1,4.5 12,7.5 7.5,0 0,1 12,4.5M12,3a9,9 0,1 0,9 9,9 9,0 0,0 -9,-9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M15.01,7.25l-1.49,0l-1.51,2.59l-1.5,-2.59l-1.5,0l1.6,2.75l-1.6,0l0,1.5l2.24,0l0,1l-2.24,0l0,1.5l2.24,0l0,2.75l1.5,0l0,-2.75l2.24,0l0,-1.5l-2.24,0l0,-1l2.24,0l0,-1.5l-1.58,0l1.6,-2.75z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,10.5A1.5,1.5 0,1 1,10.5 12,1.5 1.5,0 0,1 12,10.5M12,9a3,3 0,1 0,3 3,3 3,0 0,0 -3,-3Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M15.62,5.5h0L19.28,12l-3.66,6.5H8.38L4.72,12 8.38,5.5h7.24m0,-1.5H8.38a1.5,1.5 0,0 0,-1.31 0.77L3.41,11.26a1.55,1.55 0,0 0,0 1.48l3.66,6.49A1.5,1.5 0,0 0,8.38 20h7.24a1.5,1.5 0,0 0,1.31 -0.77l3.66,-6.49a1.55,1.55 0,0 0,0 -1.48L16.93,4.77A1.5,1.5 0,0 0,15.62 4Z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10.84,19.2V17.8a5.1,5.1 0,0 1,-5.13 -4.2,0.64 0.64,0 0,1 0.68,-0.7 0.87,0.87 0,0 1,0.82 0.69,3.66 3.66,0 0,0 3.63,2.81V15l3,2.1 -3,2.1Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M5.93,3 L3,10.52L4.61,10.52l0.62,-1.71h3l0.66,1.71h1.65L7.53,3ZM5.7,7.54l1,-2.79 1,2.79ZM20.63,17.69A2,2 0,0 0,19.57 17a1.76,1.76 0,0 0,0.78 -0.67,1.79 1.79,0 0,0 0,-1.84 2,2 0,0 0,-0.58 -0.63,1.91 1.91,0 0,0 -0.78,-0.31 8.48,8.48 0,0 0,-1.33 -0.08h-3L14.66,21h2.56c1,0 1.58,0 1.83,-0.05a2.21,2.21 0,0 0,1 -0.36,2 2,0 0,0 0.65,-0.77 2.23,2.23 0,0 0,0.24 -1A1.78,1.78 0,0 0,20.63 17.69ZM16.22,14.69h0.87c0.72,0 1.15,0 1.3,0A0.9,0.9 0,0 1,19 15a0.84,0.84 0,0 1,0.19 0.57,0.8 0.8,0 0,1 -0.22,0.59 1,1 0,0 1,-0.61 0.27c-0.14,0 -0.51,0 -1.1,0h-1ZM19.22,19.35a0.94,0.94 0,0 1,-0.55 0.3,7.41 7.41,0 0,1 -1,0L16.22,19.65v-2h1.23a5.11,5.11 0,0 1,1.34 0.11,1 1,0 0,1 0.48,0.34 1,1 0,0 1,0.16 0.58,0.93 0.93,0 0,1 -0.21,0.64Z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20.59,4.2A15,15 0,0 0,18.12 4,20 20,0 0,0 12,5 20,20 0,0 0,5.88 4a15,15 0,0 0,-2.47 0.2c-0.24,0 -0.41,0.25 -0.41,1.1L3,18.68a0.48,0.48 0,0 0,0.49 0.48h0.08A14.08,14.08 0,0 1,5.88 19a19.58,19.58 0,0 1,5.39 0.8L12,20l0.66,-0.2A19.85,19.85 0,0 1,18.12 19a14.08,14.08 0,0 1,2.31 0.18h0.08a0.48,0.48 0,0 0,0.49 -0.49L21,4.7A0.5,0.5 0,0 0,20.59 4.2ZM11.27,18.2a21.47,21.47 0,0 0,-5.39 -0.74c-0.48,0 -1,0 -1.42,0.06v-12A13.24,13.24 0,0 1,5.88 5.5a19,19 0,0 1,5.39 0.85ZM19.54,17.52c-0.46,0 -0.94,-0.06 -1.42,-0.06a21.27,21.27 0,0 0,-5.39 0.74L12.73,6.35a19.14,19.14 0,0 1,5.39 -0.85,13.11 13.11,0 0,1 1.42,0.07Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M18.12,8.45l0.5,0V7l-0.5,0a19.49,19.49 0,0 0,-4.5 0.56V9.05A18.3,18.3 0,0 1,18.12 8.45Z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20.37,4.75 L16,3H14.29A2.5,2.5 0,0 1,9.71 3H8L3.63,4.75A1,1 0,0 0,3 5.68V10a1.05,1.05 0,0 0,1 1.05,1 1,0 0,0 0.3,-0.05L6.5,10v9a2,2 0,0 0,2 2h7a2,2 0,0 0,2 -2V10l2.18,1A1,1 0,0 0,21 10V5.68A1,1 0,0 0,20.37 4.75ZM19.5,9.27 L16.72,8a0.51,0.51 0,0 0,-0.72 0.46V19a0.5,0.5 0,0 1,-0.5 0.5h-7A0.5,0.5 0,0 1,8 19V8.45A0.5,0.5 0,0 0,7.29 8L4.5,9.27V6L8.29,4.5H8.9a4,4 0,0 0,6.2 0h0.61L19.5,6Z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19.62,7.23A8.83,8.83 0,0 0,18.72 6,9 9,0 0,0 13.24,3.1 3.86,3.86 0,0 0,12.75 3c-0.25,0 -0.5,0 -0.75,0s-0.5,0 -0.75,0a3.86,3.86 0,0 0,-0.49 0.06A9,9 0,0 0,5.28 6a8.83,8.83 0,0 0,-0.9 1.21,8.93 8.93,0 0,0 0,9.54A8.83,8.83 0,0 0,5.28 18a9,9 0,0 0,5.48 2.92,3.86 3.86,0 0,0 0.49,0.06c0.25,0 0.5,0 0.75,0s0.5,0 0.75,0a3.86,3.86 0,0 0,0.49 -0.06A9,9 0,0 0,18.72 18a8.83,8.83 0,0 0,0.9 -1.21,8.93 8.93,0 0,0 0,-9.54ZM19.46,11.23L16,11.23A13.84,13.84 0,0 0,15.67 9a13.7,13.7 0,0 0,2.65 -1A7.36,7.36 0,0 1,19.46 11.25ZM17.37,6.77a12.57,12.57 0,0 1,-2.1 0.78A13.83,13.83 0,0 0,14 4.77,7.56 7.56,0 0,1 17.37,6.77ZM12,14.5a14.51,14.51 0,0 0,-2.2 0.19,12.41 12.41,0 0,1 -0.26,-1.94h4.92a12.41,12.41 0,0 1,-0.26 1.94A14.51,14.51 0,0 0,12 14.5ZM13.78,16.14A12.69,12.69 0,0 1,12 19.47a12.69,12.69 0,0 1,-1.78 -3.33A13,13 0,0 1,12 16,13 13,0 0,1 13.78,16.14ZM9.54,11.25A12.41,12.41 0,0 1,9.8 9.31,14.51 14.51,0 0,0 12,9.5a14.51,14.51 0,0 0,2.2 -0.19,12.41 12.41,0 0,1 0.26,1.94ZM10.22,7.86A12.69,12.69 0,0 1,12 4.53a12.69,12.69 0,0 1,1.78 3.33A13,13 0,0 1,12 8,13 13,0 0,1 10.22,7.86ZM10,4.77a13.83,13.83 0,0 0,-1.3 2.78,12.57 12.57,0 0,1 -2.1,-0.78A7.56,7.56 0,0 1,10 4.77ZM8.33,9A13.84,13.84 0,0 0,8 11.25L4.54,11.25A7.36,7.36 0,0 1,5.68 8,13.7 13.7,0 0,0 8.33,9ZM4.54,12.75L8,12.75A13.84,13.84 0,0 0,8.33 15a13.7,13.7 0,0 0,-2.65 1A7.36,7.36 0,0 1,4.54 12.75ZM6.63,17.23a12.57,12.57 0,0 1,2.1 -0.78A13.83,13.83 0,0 0,10 19.23,7.56 7.56,0 0,1 6.63,17.23ZM13.97,19.23a13.83,13.83 0,0 0,1.3 -2.78,12.57 12.57,0 0,1 2.1,0.78A7.56,7.56 0,0 1,14 19.23ZM15.67,15A13.84,13.84 0,0 0,16 12.75h3.5A7.36,7.36 0,0 1,18.32 16,13.7 13.7,0 0,0 15.67,15Z"/>
</vector>

@ -1,15 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="22dp"
android:height="200dp" android:height="22dp"
android:viewportWidth="200" android:viewportWidth="1024"
android:viewportHeight="200"> android:viewportHeight="1024">
<path <path
android:fillColor="#FF000000" android:fillColor="#8F000000"
android:pathData="M100.1,181.4c-10.9,0 -21.5,-2.1 -31.4,-6.3c-9.6,-4.1 -18.2,-9.9 -25.7,-17.3s-13.2,-16 -17.3,-25.7c-4.2,-10 -6.3,-20.5 -6.3,-31.4c0,-10.9 2.1,-21.5 6.3,-31.4C29.8,59.7 35.6,51 43,43.6c7.4,-7.4 16,-13.2 25.7,-17.3c10,-4.2 20.5,-6.3 31.4,-6.3s21.5,2.1 31.4,6.3c9.6,4.1 18.2,9.9 25.7,17.3c7.4,7.4 13.2,16 17.3,25.7c4.2,10 6.3,20.5 6.3,31.4s-2.1,21.5 -6.3,31.4c-4.1,9.6 -9.9,18.2 -17.3,25.7c-7.4,7.4 -16,13.2 -25.7,17.3C121.6,179.3 111,181.4 100.1,181.4zM100.1,31.6C62,31.6 31,62.6 31,100.7c0,38.1 31,69.2 69.2,69.2s69.2,-31 69.2,-69.2C169.3,62.6 138.2,31.6 100.1,31.6L100.1,31.6z" /> android:pathData="M474.84,721.55c0,21.68 17.55,38.71 38.71,38.71 21.68,0 38.71,-17.55 38.71,-38.71 0,-13.94 -7.23,-26.84 -19.61,-33.55 -11.87,-6.71 -26.84,-6.71 -38.71,0s-19.1,19.61 -19.1,33.55zM474.84,721.55M509.94,263.74C433.55,263.74 371.61,325.16 371.61,401.03c0,17.03 13.94,31.48 31.48,31.48 17.03,0 30.97,-13.94 31.48,-31.48 0,-41.81 33.03,-74.32 75.87,-74.32 41.81,0 75.87,34.06 75.87,76.39 0,16 -22.71,38.71 -42.84,58.84 -29.94,29.94 -64,64 -64,109.94v45.42c0.52,16.52 14.45,29.94 31.48,29.94s30.45,-13.42 30.97,-29.94v-45.42c0,-19.61 24.26,-44.39 45.94,-65.55 29.94,-29.94 60.9,-60.9 60.9,-102.71 -1.03,-77.42 -62.97,-139.87 -138.84,-139.87zM509.94,263.74"/>
<path <path
android:fillColor="#FF000000" android:fillColor="#8F000000"
android:pathData="M114.1,78.8c0,-7.1 -4.2,-12.9 -14,-12.9c-6.3,0 -12.2,3.1 -18,9.2l-3.5,-3.3C84.8,65.5 91,61 100.6,61c12.1,0 19.2,7.3 19.2,17.4c0,15.1 -22.3,19 -19,34.4h-5.3C92,95.9 114.1,90.9 114.1,78.8zM98.5,122.6c3.3,0 6.4,2.3 6.4,6.4c0,4.2 -3.1,6.5 -6.4,6.5c-3.4,0 -6.4,-2.3 -6.4,-6.5C92,125 95.1,122.6 98.5,122.6z" /> android:pathData="M512,157.42c195.61,0 354.06,158.97 354.58,354.58 0,195.61 -158.97,354.58 -354.58,354.58s-354.58,-158.97 -354.58,-354.58 159.48,-354.58 354.58,-354.58m0,-68.65c-233.81,0 -423.23,189.42 -423.23,423.23s189.42,423.23 423.23,423.23 423.23,-189.42 423.23,-423.23 -189.42,-423.23 -423.23,-423.23zM512,88.77"/>
<path
android:fillColor="#FF000000"
android:pathData="M98.5,138.1c-5.1,0 -8.9,-3.8 -8.9,-9c0,-5.1 3.7,-8.9 8.9,-8.9c5.1,0 8.9,3.8 8.9,8.9C107.4,134.9 102.8,138.1 98.5,138.1zM98.5,125.1c-1.8,0 -4,1 -4,4c0,3 2.2,4.1 4,4.1c2,0 4,-1.3 4,-4.1C102.5,126.3 100.4,125.1 98.5,125.1zM103.8,115.2H93.6l-0.4,-1.9c-1,-4.9 -0.2,-9.4 2.4,-13.6c2.2,-3.6 5.3,-6.5 8.3,-9.3c4.2,-3.9 7.9,-7.4 7.9,-11.6c0,-3.9 -1.5,-10.4 -11.5,-10.4c-5.5,0 -10.8,2.8 -16.2,8.4l-1.7,1.8l-7.1,-6.7l1.7,-1.8c6.8,-6.9 13.4,-11.5 23.7,-11.5c13,0 21.7,8 21.7,19.8c0,8.3 -5.7,13.6 -10.8,18.2c-5.2,4.8 -9.7,9 -8.3,15.6L103.8,115.2L103.8,115.2zM97.7,110.3h0.4c-0.3,-7.7 5.2,-12.8 10.1,-17.3c4.7,-4.4 9.2,-8.5 9.2,-14.6c0,-9.1 -6.6,-14.9 -16.8,-14.9c-0.1,0 -0.2,0 -0.3,0c10,0.1 16.2,5.9 16.2,15.3c0,6.3 -4.8,10.8 -9.4,15.2C102,98.7 97.1,103.3 97.7,110.3L97.7,110.3zM95.2,64c-3,0.7 -5.7,1.9 -8.2,3.7C89.7,65.9 92.4,64.7 95.2,64z" />
</vector> </vector>

@ -26,7 +26,7 @@
</path> </path>
<path <path
android:pathData="M108,108H54V65c0,-12 54,-5 54,-5Z" android:pathData="M108,108H54V65c0,-12 54,-5 54,-5Z"
android:fillColor="#fff" /> android:fillColor="#f2f2f2" />
<path android:pathData="M71.5,69 L78,75V58a94.89,94.89 0,0 0,-13 0.75V75Z"> <path android:pathData="M71.5,69 L78,75V58a94.89,94.89 0,0 0,-13 0.75V75Z">
<aapt:attr name="android:fillColor"> <aapt:attr name="android:fillColor">
<gradient <gradient

@ -0,0 +1,8 @@
<vector android:height="8dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
android:width="8dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#c0c0c0" android:pathData="M462.43,894.32c-242.22,0 -439.3,-197.03 -439.3,-439.3S220.16,15.78 462.43,15.78s439.3,197.03 439.3,439.3 -197.07,439.25 -439.3,439.25zM462.43,62.32c-216.58,0 -392.75,176.17 -392.75,392.75s176.17,392.75 392.75,392.75c216.53,0 392.75,-176.17 392.75,-392.75s-176.22,-392.75 -392.75,-392.75z"/>
<path android:fillColor="#c0c0c0" android:pathData="M977.64,991.98a23.37,23.37 0,0 1,-16.48 -6.8l-211.22,-211.22a23.27,23.27 0,0 1,32.91 -32.91l211.22,211.22a23.27,23.27 0,0 1,-16.43 39.7z"/>
</vector>

@ -7,7 +7,7 @@
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation_view" android:id="@+id/bottom_navigation_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="45dp"
android:elevation="10dp" android:elevation="10dp"
android:background="@color/background" android:background="@color/background"
app:labelVisibilityMode="unlabeled" app:labelVisibilityMode="unlabeled"

@ -15,7 +15,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<WebView <io.legado.app.ui.rss.read.VisibleWebView
android:id="@+id/web_view" android:id="@+id/web_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

@ -4,30 +4,35 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:paddingTop="2dp"
android:clickable="true" android:paddingRight="10dp"
android:focusable="true" android:paddingLeft="10dp">
tools:ignore="UnusedAttribute">
<io.legado.app.ui.widget.ShadowLayout
android:id="@+id/bg_cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
tools:ignore="UnusedAttribute"
app:layout_constraintTop_toTopOf="parent"
app:shadowColor="#16000000"
app:shadowShape="rectangle"
app:shadowDx="1dp"
app:shadowDy="3dp"
app:shadowRadius="4dp"
app:shadowSide="all">
<io.legado.app.ui.widget.image.CoverImageView <io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover" android:id="@+id/iv_cover"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/img_cover"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
android:transitionName="img_cover" android:transitionName="img_cover"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />
<FrameLayout </io.legado.app.ui.widget.ShadowLayout>
android:id="@+id/fl_has_new"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="@id/iv_cover"
app:layout_constraintTop_toTopOf="@id/iv_cover">
<io.legado.app.ui.widget.text.BadgeView <io.legado.app.ui.widget.text.BadgeView
android:id="@+id/bv_unread" android:id="@+id/bv_unread"
@ -36,32 +41,39 @@
android:layout_gravity="right" android:layout_gravity="right"
android:layout_margin="5dp" android:layout_margin="5dp"
android:includeFontPadding="false" android:includeFontPadding="false"
app:layout_constraintTop_toTopOf="@+id/bg_cover"
app:layout_constraintRight_toRightOf="@+id/bg_cover"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<io.legado.app.ui.widget.anima.RotateLoading <io.legado.app.ui.widget.anima.RotateLoading
android:id="@+id/rl_loading" android:id="@+id/rl_loading"
android:layout_width="26dp" android:layout_width="22dp"
android:layout_height="26dp" android:layout_height="22dp"
android:layout_gravity="right" android:layout_gravity="right"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintTop_toTopOf="@+id/bg_cover"
app:layout_constraintRight_toRightOf="@+id/bg_cover"
app:loading_color="@color/colorAccent"
app:loading_width="2dp" app:loading_width="2dp"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
</FrameLayout>
<TextView <TextView
android:id="@+id/tv_name" android:id="@+id/tv_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingBottom="4dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:gravity="center" android:gravity="top|center_horizontal"
android:lines="2" android:lines="2"
android:ellipsize="end" android:ellipsize="end"
android:text="@string/book_name" android:text="@string/book_name"
android:textColor="@color/tv_text_default" android:textColor="@color/tv_text_default"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintLeft_toLeftOf="@+id/iv_cover" app:layout_constraintLeft_toLeftOf="@+id/bg_cover"
app:layout_constraintRight_toRightOf="@+id/iv_cover" app:layout_constraintRight_toRightOf="@+id/bg_cover"
app:layout_constraintTop_toBottomOf="@+id/iv_cover" app:layout_constraintTop_toBottomOf="@+id/bg_cover"
tools:ignore="RtlHardcoded,RtlSymmetry" /> tools:ignore="RtlHardcoded,RtlSymmetry" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/bg_prefs_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:minHeight="42dp"
android:clickable="true"
android:orientation="horizontal"
android:focusable="true">
<ImageView
android:id="@+id/preference_icon"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
android:visibility="visible"
android:paddingTop="1dp"
android:layout_width="24dp"
android:layout_height="24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
<LinearLayout
android:orientation="vertical"
android:layout_weight="1.0"
android:layout_width="0dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/preference_title"
android:paddingBottom="8dp"
android:singleLine="true"
android:textSize="16sp"
android:text="@string/title"
android:textColor="@color/tv_text_default"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/preference_desc"
android:paddingBottom="8dp"
android:textSize="14sp"
android:textColor="@color/tv_text_summary"
android:text="@string/default1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<FrameLayout
android:id="@+id/preference_widget"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:gravity="right|center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="RtlHardcoded" />
</LinearLayout>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<View
android:id="@+id/preference_divider_above"
android:layout_width="match_parent"
android:layout_height="8dp" />
<TextView
android:id="@+id/preference_title"
android:paddingTop="16dip"
android:paddingBottom="8dip"
android:background="@drawable/bg_prefs_color"
android:paddingLeft="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<View
android:id="@+id/preference_divider_below"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="8dp" />
</LinearLayout>

@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.SearchView <io.legado.app.ui.widget.SearchView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_view" android:id="@+id/search_view"
android:theme="?attr/actionBarStyle" android:theme="?attr/actionBarStyle"
android:background="@drawable/bg_searchview" android:background="@drawable/bg_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="34dp" android:layout_height="30dp"
android:layout_marginTop="8dp" android:layout_marginTop="10dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="10dp"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:focusable="false"
app:queryBackground="@null" app:queryBackground="@null"
app:submitBackground="@null" app:submitBackground="@null"
app:searchHintIcon="@drawable/ic_search_hint"
app:defaultQueryHint="搜索"/> app:defaultQueryHint="搜索"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@ -7,6 +7,7 @@
<color name="background">@color/md_grey_900</color> <color name="background">@color/md_grey_900</color>
<color name="background_card">@color/md_grey_850</color> <color name="background_card">@color/md_grey_850</color>
<color name="background_menu">@color/md_grey_800</color> <color name="background_menu">@color/md_grey_800</color>
<color name="background_prefs">#10303030</color>
<color name="night_mask">#69000000</color> <color name="night_mask">#69000000</color>

@ -173,4 +173,17 @@
<attr name="bgColor" format="color" /> <attr name="bgColor" format="color" />
<attr name="arcDirectionTop" format="boolean" /> <attr name="arcDirectionTop" format="boolean" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ShadowLayout"><attr format="color" name="shadowColor"/><attr format="dimension" name="shadowRadius"/><attr format="dimension" name="shadowDx"/><attr format="dimension" name="shadowDy"/><attr name="shadowShape">
<flag name="rectangle" value="0x0001"/>
<flag name="oval" value="0x0010"/>
</attr><attr name="shadowSide">
<flag name="all" value="0x1111"/>
<flag name="left" value="0x0001"/>
<flag name="top" value="0x0010"/>
<flag name="right" value="0x0100"/>
<flag name="bottom" value="0x1000"/>
</attr>
</declare-styleable>
</resources> </resources>

@ -16,6 +16,7 @@
<color name="background">@color/md_grey_50</color> <color name="background">@color/md_grey_50</color>
<color name="background_card">@color/md_grey_100</color> <color name="background_card">@color/md_grey_100</color>
<color name="background_menu">@color/md_grey_200</color> <color name="background_menu">@color/md_grey_200</color>
<color name="background_prefs">@color/md_grey_50</color>
<color name="transparent">#00000000</color> <color name="transparent">#00000000</color>
<color name="transparent10">#10000000</color> <color name="transparent10">#10000000</color>

@ -29,6 +29,7 @@
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="replace">替换</string> <string name="replace">替换</string>
<string name="replace_purify">替换净化</string> <string name="replace_purify">替换净化</string>
<string name="replace_purify_desc">配置替换净化规则</string>
<string name="not_available">暂无</string> <string name="not_available">暂无</string>
<string name="enable">启用</string> <string name="enable">启用</string>
<string name="replace_purify_search">替换净化-搜索</string> <string name="replace_purify_search">替换净化-搜索</string>
@ -58,6 +59,7 @@
<string name="book_local">添加本地</string> <string name="book_local">添加本地</string>
<string name="book_source">书源</string> <string name="book_source">书源</string>
<string name="book_source_manage">书源管理</string> <string name="book_source_manage">书源管理</string>
<string name="book_source_manage_desc">新建/导入/编辑/管理书源</string>
<string name="setting">设置</string> <string name="setting">设置</string>
<string name="theme_setting">主题设置</string> <string name="theme_setting">主题设置</string>
<string name="theme_setting_s">与界面/颜色相关的一些设置</string> <string name="theme_setting_s">与界面/颜色相关的一些设置</string>
@ -80,6 +82,7 @@
<string name="is_loading">加载中…</string> <string name="is_loading">加载中…</string>
<string name="retry">重试</string> <string name="retry">重试</string>
<string name="web_service">Web 服务</string> <string name="web_service">Web 服务</string>
<string name="web_service_desc">启用Web服务</string>
<string name="web_edit_source">web编辑书源</string> <string name="web_edit_source">web编辑书源</string>
<string name="http_ip">http://%1$s:%2$d</string> <string name="http_ip">http://%1$s:%2$d</string>
<string name="download_offline">离线下载</string> <string name="download_offline">离线下载</string>
@ -442,6 +445,7 @@
<string name="click_on_selected_show_menu">选中时点击可弹出菜单</string> <string name="click_on_selected_show_menu">选中时点击可弹出菜单</string>
<string name="theme">主题</string> <string name="theme">主题</string>
<string name="theme_mode">主题模式</string> <string name="theme_mode">主题模式</string>
<string name="theme_mode_desc">选择主题模式</string>
<string name="default_theme">默认主题</string> <string name="default_theme">默认主题</string>
<string name="restore_default_theme">恢复主题为默认配色</string> <string name="restore_default_theme">恢复主题为默认配色</string>
<string name="join_qq_group">加入QQ群</string> <string name="join_qq_group">加入QQ群</string>
@ -457,6 +461,7 @@
<string name="import_per">导入本地书籍需存储权限</string> <string name="import_per">导入本地书籍需存储权限</string>
<string name="night_theme">夜间模式</string> <string name="night_theme">夜间模式</string>
<string name="eink_theme">E-Ink 模式</string> <string name="eink_theme">E-Ink 模式</string>
<string name="eink_theme_desc">电子墨水屏模式</string>
<string name="get_storage_per">本软件需要存储权限来存储备份书籍信息</string> <string name="get_storage_per">本软件需要存储权限来存储备份书籍信息</string>
<string name="double_click_exit">再按一次退出程序</string> <string name="double_click_exit">再按一次退出程序</string>
<string name="import_book_per">导入本地书籍需存储权限</string> <string name="import_book_per">导入本地书籍需存储权限</string>

@ -2,68 +2,78 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="contributors" android:key="contributors"
android:title="开发人员" android:title="开发人员"
android:summary="gedoor,Invinciblelee等, 详情请在github中查看" android:summary="gedoor,Invinciblelee等, 详情请在github中查看"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="check_update" android:key="check_update"
android:title="@string/check_update" android:title="@string/check_update"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="update_log" android:key="update_log"
android:title="@string/update_log" android:title="@string/update_log"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="联系我们" android:title="联系我们"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="gzGzh" android:key="gzGzh"
android:title="关注公众号" android:title="关注公众号"
android:summary="开源阅读软件" android:summary="开源阅读软件"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="qq" android:key="qq"
android:title="@string/join_qq_group" android:title="@string/join_qq_group"
android:summary="点击加入" android:summary="点击加入"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="mail" android:key="mail"
android:title="@string/send_mail" android:title="@string/send_mail"
android:summary="kunfei.ge@gmail.com" android:summary="kunfei.ge@gmail.com"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="git" android:key="git"
android:title="@string/git_hub" android:title="@string/git_hub"
android:summary="@string/this_github_url" android:summary="@string/this_github_url"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="home_page" android:key="home_page"
android:title="@string/home_page" android:title="@string/home_page"
android:summary="@string/home_page_url" android:summary="@string/home_page_url"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="其它" android:title="其它"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="license" android:key="license"
android:title="开源许可" android:title="开源许可"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="disclaimer" android:key="disclaimer"
android:title="@string/disclaimer" android:title="@string/disclaimer"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />

@ -4,51 +4,64 @@
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="微信" android:title="微信"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="gzGzh" android:key="gzGzh"
android:title="关注公众号" android:title="关注公众号"
android:summary="关注[开源阅读软件]点击广告支持我" android:summary="关注[开源阅读软件]点击广告支持我"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="wxZsm" android:key="wxZsm"
android:title="微信赞赏码" android:title="微信赞赏码"
android:summary="点击打开" android:summary="点击打开"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="支付宝" android:title="支付宝"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="zfbHbSsm" android:key="zfbHbSsm"
android:title="支付宝红包搜索码" android:title="支付宝红包搜索码"
android:summary="537954522 点击复制" android:summary="537954522 点击复制"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="zfbHbRwm" android:key="zfbHbRwm"
android:title="支付宝红包二维码" android:title="支付宝红包二维码"
android:summary="点击打开" android:summary="点击打开"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="zfbSkRwm" android:key="zfbSkRwm"
android:title="支付宝收款二维码" android:title="支付宝收款二维码"
android:summary="点击打开" android:summary="点击打开"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="QQ" android:title="QQ"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="qqSkRwm" android:key="qqSkRwm"
android:title="QQ收款二维码" android:title="QQ收款二维码"
android:summary="点击打开" android:summary="点击打开"

@ -4,21 +4,24 @@
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="@string/web_dav_set" android:title="@string/web_dav_set"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.EditTextPreference <io.legado.app.ui.widget.prefs.EditTextPreference
android:key="web_dav_url" android:key="web_dav_url"
android:title="WebDav 服务器地址" android:title="WebDav 服务器地址"
android:summary="@string/web_dav_url_s" android:summary="@string/web_dav_url_s"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.EditTextPreference <io.legado.app.ui.widget.prefs.EditTextPreference
android:key="web_dav_account" android:key="web_dav_account"
android:title="WebDav 账号" android:title="WebDav 账号"
android:summary="@string/web_dav_account_s" android:summary="@string/web_dav_account_s"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.EditTextPreference <io.legado.app.ui.widget.prefs.EditTextPreference
android:key="web_dav_password" android:key="web_dav_password"
android:title="WebDav 密码" android:title="WebDav 密码"
android:summary="@string/web_dav_pw_s" android:summary="@string/web_dav_pw_s"
@ -29,28 +32,33 @@
android:defaultValue="true" android:defaultValue="true"
android:title="创建子文件夹" android:title="创建子文件夹"
android:summary="创建legado文件夹作为备份文件夹" android:summary="创建legado文件夹作为备份文件夹"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory app:iconSpaceReserved="false"> <io.legado.app.ui.widget.prefs.PreferenceCategory
app:layout="@layout/view_preference_category"
app:allowDividerBelow="false"
app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="backupUri" android:key="backupUri"
android:title="备份路径" android:title="备份路径"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="web_dav_backup" android:key="web_dav_backup"
android:title="备份" android:title="备份"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="web_dav_restore" android:key="web_dav_restore"
android:title="恢复" android:title="恢复"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="import_old" android:key="import_old"
android:title="导入旧版本数据" android:title="导入旧版本数据"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />

@ -4,6 +4,9 @@
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="@string/main_activity" android:title="@string/main_activity"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
@ -24,30 +27,35 @@
android:defaultValue="true" android:defaultValue="true"
android:key="showRss" android:key="showRss"
android:title="@string/show_rss" android:title="@string/show_rss"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="@string/other_setting" android:title="@string/other_setting"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<Preference <io.legado.app.ui.widget.prefs.Preference
android:key="threadCount" android:key="threadCount"
android:title="@string/threads_num_title" android:title="@string/threads_num_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <io.legado.app.ui.widget.prefs.Preference
android:key="webPort" android:key="webPort"
android:title="@string/web_port_title" android:title="@string/web_port_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <io.legado.app.ui.widget.prefs.Preference
android:key="downloadPath" android:key="downloadPath"
android:title="@string/download_path" android:title="@string/download_path"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <io.legado.app.ui.widget.prefs.Preference
android:key="cleanCache" android:key="cleanCache"
android:title="@string/cleanCache" android:title="@string/cleanCache"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />

@ -26,21 +26,26 @@
android:title="@string/navigation_bar_color_change" android:title="@string/navigation_bar_color_change"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:defaultValue="true" android:defaultValue="true"
android:key="barElevation" android:key="barElevation"
android:summary="@string/bar_elevation_s" android:summary="@string/bar_elevation_s"
android:title="@string/bar_elevation" android:title="@string/bar_elevation"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="defaultTheme" android:key="defaultTheme"
android:summary="@string/restore_default_theme" android:summary="@string/restore_default_theme"
android:title="@string/default_theme" android:title="@string/default_theme"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="白天" android:title="白天"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference
@ -73,12 +78,17 @@
android:summary="白天,底栏色" android:summary="白天,底栏色"
android:title="底部操作栏颜色" android:title="底部操作栏颜色"
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="夜间" android:title="夜间"
app:layout="@layout/view_preference_category"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference

@ -1,15 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
android:divider="@color/transparent"
app:allowDividerBelow="false"
app:allowDividerAbove="false"
>
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="bookSourceManage" android:key="bookSourceManage"
android:title="@string/book_source_manage" android:title="@string/book_source_manage"
android:summary="@string/book_source_manage_desc"
android:icon="@drawable/ic_cfg_source"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="replaceManage" android:key="replaceManage"
android:title="@string/replace_purify" android:title="@string/replace_purify"
android:summary="@string/replace_purify_desc"
android:icon="@drawable/ic_cfg_replace"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.NameListPreference <io.legado.app.ui.widget.prefs.NameListPreference
@ -18,58 +26,76 @@
android:entryValues="@array/theme_mode_v" android:entryValues="@array/theme_mode_v"
android:key="themeMode" android:key="themeMode"
android:title="@string/theme_mode" android:title="@string/theme_mode"
android:summary="@string/theme_mode_desc"
android:icon="@drawable/ic_cfg_theme"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="isEInkMode" android:key="isEInkMode"
android:title="@string/eink_theme" android:title="@string/eink_theme"
android:summary="@string/eink_theme_desc"
android:icon="@drawable/ic_cfg_e_lnk"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="webService" android:key="webService"
android:title="@string/web_service" android:title="@string/web_service"
android:summary="@string/web_service_desc"
android:icon="@drawable/ic_cfg_web"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:title="@string/setting" android:title="@string/setting"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false"
app:allowDividerAbove="true"
app:allowDividerBelow="false" >
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="web_dav_setting" android:key="web_dav_setting"
android:summary="@string/web_dav_set_import_old" android:summary="@string/web_dav_set_import_old"
android:title="@string/backup_restore" android:title="@string/backup_restore"
android:icon="@drawable/ic_cfg_backup"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="theme_setting" android:key="theme_setting"
android:summary="@string/theme_setting_s" android:summary="@string/theme_setting_s"
android:title="@string/theme_setting" android:title="@string/theme_setting"
android:icon="@drawable/ic_cfg_theme"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="setting" android:key="setting"
android:summary="@string/other_setting_s" android:summary="@string/other_setting_s"
android:title="@string/other_setting" android:title="@string/other_setting"
android:icon="@drawable/ic_cfg_other"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:key="aboutCategory" android:key="aboutCategory"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:layout="@layout/view_preference_category"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="donate" android:key="donate"
android:summary="您的支持是我更新的动力" android:summary="您的支持是我更新的动力"
android:title="@string/donate" android:title="@string/donate"
android:icon="@drawable/ic_cfg_jz"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="about" android:key="about"
android:summary="公众号[开源阅读软件]" android:summary="公众号[开源阅读软件]"
android:title="@string/about" android:title="@string/about"
android:icon="@drawable/ic_cfg_about"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>

Loading…
Cancel
Save