Merge branch 'master' into master
This commit is contained in:
@@ -14,6 +14,7 @@ import io.legado.app.constant.AppConst.channelIdReadAloud
|
||||
import io.legado.app.constant.AppConst.channelIdWeb
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.LifecycleHelp
|
||||
import io.legado.app.help.RuleBigDataHelp
|
||||
@@ -52,6 +53,7 @@ class App : MultiDexApplication() {
|
||||
appDb.searchBookDao.clearExpired(clearTime)
|
||||
}
|
||||
RuleBigDataHelp.clearInvalid()
|
||||
BookHelp.clearInvalidCache()
|
||||
//初始化简繁转换引擎
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> ChineseUtils.t2s("初始化")
|
||||
|
||||
@@ -7,7 +7,6 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.msg
|
||||
|
||||
object BookSourceController {
|
||||
|
||||
@@ -23,20 +22,16 @@ object BookSourceController {
|
||||
fun saveSource(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
postData ?: return returnData.setErrorMsg("数据不能为空")
|
||||
kotlin.runCatching {
|
||||
val bookSource = BookSource.fromJson(postData)
|
||||
if (bookSource != null) {
|
||||
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
returnData.setData("")
|
||||
}
|
||||
val bookSource = BookSource.fromJson(postData).getOrNull()
|
||||
if (bookSource != null) {
|
||||
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
returnData.setErrorMsg("转换源失败")
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
returnData.setData("")
|
||||
}
|
||||
}.onFailure {
|
||||
returnData.setErrorMsg(it.msg)
|
||||
} else {
|
||||
returnData.setErrorMsg("转换源失败")
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
@@ -44,19 +39,18 @@ object BookSourceController {
|
||||
fun saveSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("数据为空")
|
||||
val okSources = arrayListOf<BookSource>()
|
||||
val bookSources = BookSource.fromJsonArray(postData)
|
||||
if (bookSources.isNotEmpty()) {
|
||||
bookSources.forEach { bookSource ->
|
||||
if (bookSource.bookSourceName.isNotBlank()
|
||||
&& bookSource.bookSourceUrl.isNotBlank()
|
||||
) {
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
okSources.add(bookSource)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val bookSources = BookSource.fromJsonArray(postData).getOrNull()
|
||||
if (bookSources.isNullOrEmpty()) {
|
||||
return ReturnData().setErrorMsg("转换源失败")
|
||||
}
|
||||
bookSources.forEach { bookSource ->
|
||||
if (bookSource.bookSourceName.isNotBlank()
|
||||
&& bookSource.bookSourceUrl.isNotBlank()
|
||||
) {
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
okSources.add(bookSource)
|
||||
}
|
||||
}
|
||||
return ReturnData().setData(okSources)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.text.TextUtils
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.utils.msg
|
||||
|
||||
object RssSourceController {
|
||||
|
||||
@@ -21,20 +20,15 @@ object RssSourceController {
|
||||
fun saveSource(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
postData ?: return returnData.setErrorMsg("数据不能为空")
|
||||
kotlin.runCatching {
|
||||
val source = RssSource.fromJson(postData)
|
||||
if (source != null) {
|
||||
if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
appDb.rssSourceDao.insert(source)
|
||||
returnData.setData("")
|
||||
}
|
||||
RssSource.fromJson(postData).onFailure {
|
||||
returnData.setErrorMsg("转换源失败${it.localizedMessage}")
|
||||
}.onSuccess { source ->
|
||||
if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
returnData.setErrorMsg("转换源失败")
|
||||
appDb.rssSourceDao.insert(source)
|
||||
returnData.setData("")
|
||||
}
|
||||
}.onFailure {
|
||||
returnData.setErrorMsg(it.msg)
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
@@ -42,18 +36,17 @@ object RssSourceController {
|
||||
fun saveSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("数据不能为空")
|
||||
val okSources = arrayListOf<RssSource>()
|
||||
val source = RssSource.fromJsonArray(postData)
|
||||
if (source.isNotEmpty()) {
|
||||
for (rssSource in source) {
|
||||
if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
|
||||
continue
|
||||
}
|
||||
appDb.rssSourceDao.insert(rssSource)
|
||||
okSources.add(rssSource)
|
||||
}
|
||||
} else {
|
||||
val source = RssSource.fromJsonArray(postData).getOrNull()
|
||||
if (source.isNullOrEmpty()) {
|
||||
return ReturnData().setErrorMsg("转换源失败")
|
||||
}
|
||||
for (rssSource in source) {
|
||||
if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
|
||||
continue
|
||||
}
|
||||
appDb.rssSourceDao.insert(rssSource)
|
||||
okSources.add(rssSource)
|
||||
}
|
||||
return ReturnData().setData(okSources)
|
||||
}
|
||||
|
||||
@@ -70,11 +63,11 @@ object RssSourceController {
|
||||
|
||||
fun deleteSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("没有传递数据")
|
||||
kotlin.runCatching {
|
||||
RssSource.fromJsonArray(postData).let {
|
||||
it.forEach { source ->
|
||||
appDb.rssSourceDao.delete(source)
|
||||
}
|
||||
RssSource.fromJsonArray(postData).onFailure {
|
||||
return ReturnData().setErrorMsg("格式不对")
|
||||
}.onSuccess {
|
||||
it.forEach { source ->
|
||||
appDb.rssSourceDao.delete(source)
|
||||
}
|
||||
}
|
||||
return ReturnData().setData("已执行"/*okSources*/)
|
||||
|
||||
@@ -22,7 +22,7 @@ object AppPattern {
|
||||
val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
|
||||
|
||||
//本地书籍支持类型
|
||||
val bookFileRegex = Regex("(?i).*\\.(txt|epub|umd)")
|
||||
val bookFileRegex = Regex(".*\\.(txt|epub|umd)", RegexOption.IGNORE_CASE)
|
||||
|
||||
/**
|
||||
* 所有标点
|
||||
|
||||
@@ -12,15 +12,26 @@ interface BookSourceDao {
|
||||
|
||||
@Query(
|
||||
"""select * from book_sources
|
||||
where bookSourceName like :searchKey
|
||||
or bookSourceGroup like :searchKey
|
||||
or bookSourceUrl like :searchKey
|
||||
or bookSourceComment like :searchKey
|
||||
where bookSourceName like '%' || :searchKey || '%'
|
||||
or bookSourceGroup like '%' || :searchKey || '%'
|
||||
or bookSourceUrl like '%' || :searchKey || '%'
|
||||
or bookSourceComment like '%' || :searchKey || '%'
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowSearch(searchKey: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
|
||||
@Query(
|
||||
"""select * from book_sources
|
||||
where enabled = 1 and
|
||||
(bookSourceName like '%' || :searchKey || '%'
|
||||
or bookSourceGroup like '%' || :searchKey || '%'
|
||||
or bookSourceUrl like '%' || :searchKey || '%'
|
||||
or bookSourceComment like '%' || :searchKey || '%')
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowSearchEnabled(searchKey: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where bookSourceGroup like '%' || :searchKey || '%' order by customOrder asc")
|
||||
fun flowGroupSearch(searchKey: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
|
||||
@@ -39,7 +50,7 @@ interface BookSourceDao {
|
||||
"""select * from book_sources
|
||||
where enabledExplore = 1
|
||||
and trim(exploreUrl) <> ''
|
||||
and (bookSourceGroup like :key or bookSourceName like :key)
|
||||
and (bookSourceGroup like '%' || :key || '%' or bookSourceName like '%' || :key || '%')
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowExplore(key: String): Flow<List<BookSource>>
|
||||
@@ -48,7 +59,7 @@ interface BookSourceDao {
|
||||
"""select * from book_sources
|
||||
where enabledExplore = 1
|
||||
and trim(exploreUrl) <> ''
|
||||
and (bookSourceGroup like :key)
|
||||
and (bookSourceGroup like '%' || :key || '%')
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowGroupExplore(key: String): Flow<List<BookSource>>
|
||||
@@ -74,6 +85,9 @@ interface BookSourceDao {
|
||||
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
|
||||
fun getEnabledByGroup(group: String): List<BookSource>
|
||||
|
||||
@Query("select * from book_sources where enabled = 1 and bookSourceType = :type")
|
||||
fun getEnabledByType(type: Int): List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources where trim(bookUrlPattern) <> '' order by enabled desc, customOrder")
|
||||
val hasBookUrlPattern: List<BookSource>
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ import io.legado.app.data.entities.Bookmark
|
||||
@Dao
|
||||
interface BookmarkDao {
|
||||
|
||||
@get:Query("select * from bookmarks")
|
||||
@get:Query(
|
||||
"""
|
||||
select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos
|
||||
"""
|
||||
)
|
||||
val all: List<Bookmark>
|
||||
|
||||
@Query(
|
||||
|
||||
@@ -6,6 +6,8 @@ import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.PageAnim
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.model.ReadBook
|
||||
@@ -271,7 +273,13 @@ data class Book(
|
||||
this.tocHtml = this@Book.tocHtml
|
||||
}
|
||||
|
||||
fun changeTo(newBook: Book) {
|
||||
fun changeTo(newBook: Book, toc: List<BookChapter>): Book {
|
||||
newBook.durChapterIndex = BookHelp
|
||||
.getDurChapter(durChapterIndex, durChapterTitle, toc, totalChapterNum)
|
||||
newBook.durChapterTitle = toc[newBook.durChapterIndex].getDisplayTitle(
|
||||
ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules()
|
||||
)
|
||||
newBook.durChapterPos = durChapterPos
|
||||
newBook.group = group
|
||||
newBook.order = order
|
||||
newBook.customCoverUrl = customCoverUrl
|
||||
@@ -279,23 +287,11 @@ data class Book(
|
||||
newBook.customTag = customTag
|
||||
newBook.canUpdate = canUpdate
|
||||
newBook.readConfig = readConfig
|
||||
delete(this)
|
||||
appDb.bookDao.insert(newBook)
|
||||
}
|
||||
|
||||
fun upInfoFromOld(oldBook: Book?) {
|
||||
oldBook?.let {
|
||||
group = oldBook.group
|
||||
durChapterIndex = oldBook.durChapterIndex
|
||||
durChapterPos = oldBook.durChapterPos
|
||||
durChapterTitle = oldBook.durChapterTitle
|
||||
customCoverUrl = oldBook.customCoverUrl
|
||||
customIntro = oldBook.customIntro
|
||||
order = oldBook.order
|
||||
if (coverUrl.isNullOrEmpty()) {
|
||||
coverUrl = oldBook.getDisplayCover()
|
||||
}
|
||||
if (appDb.bookDao.has(bookUrl) == true) {
|
||||
delete()
|
||||
appDb.bookDao.insert(newBook)
|
||||
}
|
||||
return newBook
|
||||
}
|
||||
|
||||
fun createBookMark(): Bookmark {
|
||||
@@ -313,20 +309,19 @@ data class Book(
|
||||
}
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
if (ReadBook.book?.bookUrl == bookUrl) {
|
||||
ReadBook.book = null
|
||||
}
|
||||
appDb.bookDao.delete(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val hTag = 2L
|
||||
const val rubyTag = 4L
|
||||
const val imgStyleDefault = "DEFAULT"
|
||||
const val imgStyleFull = "FULL"
|
||||
const val imgStyleText = "TEXT"
|
||||
|
||||
fun delete(book: Book?) {
|
||||
book ?: return
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = null
|
||||
}
|
||||
appDb.bookDao.delete(book)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.legado.app.utils.*
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
import java.io.InputStream
|
||||
|
||||
@Parcelize
|
||||
@TypeConverters(BookSource.Converters::class)
|
||||
@@ -203,13 +204,17 @@ data class BookSource(
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromJson(json: String): BookSource? {
|
||||
fun fromJson(json: String): Result<BookSource> {
|
||||
return SourceAnalyzer.jsonToBookSource(json)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): List<BookSource> {
|
||||
fun fromJsonArray(json: String): Result<MutableList<BookSource>> {
|
||||
return SourceAnalyzer.jsonToBookSources(json)
|
||||
}
|
||||
|
||||
fun fromJsonArray(inputStream: InputStream): Result<MutableList<BookSource>> {
|
||||
return SourceAnalyzer.jsonToBookSources(inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
class Converters {
|
||||
|
||||
@@ -121,7 +121,7 @@ data class RssSource(
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
companion object {
|
||||
|
||||
fun fromJsonDoc(doc: DocumentContext): RssSource? {
|
||||
fun fromJsonDoc(doc: DocumentContext): Result<RssSource> {
|
||||
return kotlin.runCatching {
|
||||
val loginUi = doc.read<Any>("$.loginUi")
|
||||
RssSource(
|
||||
@@ -152,23 +152,25 @@ data class RssSource(
|
||||
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
||||
customOrder = doc.readInt("$.customOrder") ?: 0
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJson(json: String): RssSource? {
|
||||
fun fromJson(json: String): Result<RssSource> {
|
||||
return fromJsonDoc(jsonPath.parse(json))
|
||||
}
|
||||
|
||||
fun fromJsonArray(jsonArray: String): ArrayList<RssSource> {
|
||||
val sources = arrayListOf<RssSource>()
|
||||
val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
|
||||
doc.forEach {
|
||||
val jsonItem = jsonPath.parse(it)
|
||||
fromJsonDoc(jsonItem)?.let { source ->
|
||||
sources.add(source)
|
||||
fun fromJsonArray(jsonArray: String): Result<ArrayList<RssSource>> {
|
||||
return kotlin.runCatching {
|
||||
val sources = arrayListOf<RssSource>()
|
||||
val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
|
||||
doc.forEach {
|
||||
val jsonItem = jsonPath.parse(it)
|
||||
fromJsonDoc(jsonItem).getOrThrow().let { source ->
|
||||
sources.add(source)
|
||||
}
|
||||
}
|
||||
sources
|
||||
}
|
||||
return sources
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,11 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import org.apache.commons.text.similarity.JaccardSimilarity
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
@@ -44,11 +41,10 @@ object BookHelp {
|
||||
/**
|
||||
* 清除已删除书的缓存
|
||||
*/
|
||||
fun clearRemovedCache() {
|
||||
Coroutine.async {
|
||||
val bookFolderNames = arrayListOf<String>()
|
||||
appDb.bookDao.all.forEach {
|
||||
bookFolderNames.add(it.getFolderName())
|
||||
suspend fun clearInvalidCache() {
|
||||
withContext(IO) {
|
||||
val bookFolderNames = appDb.bookDao.all.map {
|
||||
it.getFolderName()
|
||||
}
|
||||
val file = downloadDir.getFile(cacheFolderName)
|
||||
file.listFiles()?.forEach { bookFile ->
|
||||
|
||||
@@ -54,13 +54,11 @@ object DefaultData {
|
||||
}
|
||||
|
||||
val rssSources: List<RssSource> by lazy {
|
||||
kotlin.runCatching {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}rssSources.json")
|
||||
.readBytes()
|
||||
)
|
||||
RssSource.fromJsonArray(json)
|
||||
}.getOrDefault(emptyList())
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}rssSources.json")
|
||||
.readBytes()
|
||||
)
|
||||
RssSource.fromJsonArray(json).getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
val coverRuleConfig: BookCover.CoverRuleConfig by lazy {
|
||||
|
||||
@@ -139,7 +139,7 @@ interface JsExtensions {
|
||||
path.startsWith("/storage") -> FileUtils.readText(path)
|
||||
else -> readTxtFile(path)
|
||||
}
|
||||
if (result.isBlank()) throw NoStackTraceException("${path} 内容获取失败或者为空")
|
||||
if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空")
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.rule.*
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.utils.*
|
||||
import java.io.InputStream
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -16,31 +18,66 @@ object SourceAnalyzer {
|
||||
private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE)
|
||||
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
fun jsonToBookSources(json: String): List<BookSource> {
|
||||
val bookSources = mutableListOf<BookSource>()
|
||||
if (json.isJsonArray()) {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
fun jsonToBookSources(json: String): Result<MutableList<BookSource>> {
|
||||
return kotlin.runCatching {
|
||||
val bookSources = mutableListOf<BookSource>()
|
||||
when {
|
||||
json.isJsonArray() -> {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
|
||||
bookSources.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
json.isJsonObject() -> {
|
||||
jsonToBookSource(json).getOrThrow().let {
|
||||
bookSources.add(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw NoStackTraceException("格式不对")
|
||||
}
|
||||
}
|
||||
bookSources
|
||||
}
|
||||
}
|
||||
|
||||
fun jsonToBookSources(inputStream: InputStream): Result<MutableList<BookSource>> {
|
||||
return kotlin.runCatching {
|
||||
val bookSources = mutableListOf<BookSource>()
|
||||
kotlin.runCatching {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(inputStream).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
|
||||
bookSources.add(it)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
val item: Map<String, Any> = jsonPath.parse(inputStream).read("$")
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToBookSource(jsonItem.jsonString())?.let {
|
||||
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
|
||||
bookSources.add(it)
|
||||
}
|
||||
}
|
||||
bookSources
|
||||
}
|
||||
return bookSources
|
||||
}
|
||||
|
||||
fun jsonToBookSource(json: String): BookSource? {
|
||||
fun jsonToBookSource(json: String): Result<BookSource> {
|
||||
val source = BookSource()
|
||||
val sourceAny = GSON.fromJsonObject<BookSourceAny>(json.trim())
|
||||
.onFailure {
|
||||
AppLog.put("转化书源出错", it)
|
||||
}.getOrNull()
|
||||
try {
|
||||
return kotlin.runCatching {
|
||||
if (sourceAny?.ruleToc == null) {
|
||||
source.apply {
|
||||
val jsonItem = jsonPath.parse(json.trim())
|
||||
bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: return null
|
||||
bookSourceUrl = jsonItem.readString("bookSourceUrl")
|
||||
?: throw NoStackTraceException("格式不对")
|
||||
bookSourceName = jsonItem.readString("bookSourceName") ?: ""
|
||||
bookSourceGroup = jsonItem.readString("bookSourceGroup")
|
||||
loginUrl = jsonItem.readString("loginUrl")
|
||||
@@ -168,10 +205,8 @@ object SourceAnalyzer {
|
||||
.getOrNull()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
source
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
@@ -301,6 +301,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val doublePageHorizontal: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.doublePageHorizontal, true)
|
||||
|
||||
var searchGroup: String
|
||||
get() = appCtx.getPrefString("searchGroup") ?: ""
|
||||
set(value) {
|
||||
appCtx.putPrefString("searchGroup", value)
|
||||
}
|
||||
|
||||
private fun getPrefUserAgent(): String {
|
||||
val ua = appCtx.getPrefString(PreferKey.userAgent)
|
||||
if (ua.isNullOrBlank()) {
|
||||
|
||||
@@ -91,9 +91,10 @@ object ImportOldData {
|
||||
}
|
||||
|
||||
fun importOldSource(json: String): Int {
|
||||
val bookSources = BookSource.fromJsonArray(json)
|
||||
appDb.bookSourceDao.insert(*bookSources.toTypedArray())
|
||||
return bookSources.size
|
||||
val count = BookSource.fromJsonArray(json).onSuccess {
|
||||
appDb.bookSourceDao.insert(*it.toTypedArray())
|
||||
}.getOrNull()?.size
|
||||
return count ?: 0
|
||||
}
|
||||
|
||||
private fun importOldReplaceRule(json: String): Int {
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
|
||||
object Restore {
|
||||
@@ -192,8 +193,9 @@ object Restore {
|
||||
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
|
||||
try {
|
||||
val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
|
||||
val json = file.readText()
|
||||
return GSON.fromJsonArray<T>(json).getOrThrow()
|
||||
FileInputStream(file).use {
|
||||
return GSON.fromJsonArray<T>(it).getOrThrow()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("$fileName\n读取解析出错\n${e.localizedMessage}", e)
|
||||
appCtx.toastOnUi("$fileName\n读取文件出错\n${e.localizedMessage}")
|
||||
|
||||
@@ -48,9 +48,9 @@ fun Context.alert(
|
||||
|
||||
inline fun Fragment.alert(
|
||||
titleResource: Int? = null,
|
||||
message: Int? = null,
|
||||
messageResource: Int? = null,
|
||||
noinline init: (AlertBuilder<DialogInterface>.() -> Unit)? = null
|
||||
) = requireActivity().alert(titleResource, message, init)
|
||||
) = requireActivity().alert(titleResource, messageResource, init)
|
||||
|
||||
fun Context.alert(init: AlertBuilder<AlertDialog>.() -> Unit): AlertDialog =
|
||||
AndroidAlertBuilder(this).apply {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.lib.webdav
|
||||
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
@@ -168,6 +169,8 @@ class WebDav(urlStr: String) {
|
||||
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
|
||||
}.close()
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put(it.localizedMessage)
|
||||
}.isSuccess
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -22,6 +22,7 @@ import javax.script.SimpleBindings
|
||||
object LocalBook {
|
||||
|
||||
private val nameAuthorPatterns = arrayOf(
|
||||
Pattern.compile("(.*?)《([^《》]+)》.*?作者:(.*)"),
|
||||
Pattern.compile("(.*?)《([^《》]+)》(.*)"),
|
||||
Pattern.compile("(^)(.+) 作者:(.+)$"),
|
||||
Pattern.compile("(^)(.+) by (.+)$")
|
||||
|
||||
@@ -8,7 +8,6 @@ import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
@@ -60,7 +59,7 @@ class SearchModel(private val scope: CoroutineScope) {
|
||||
initSearchPool()
|
||||
mSearchId = searchId
|
||||
searchPage = 1
|
||||
val searchGroup = appCtx.getPrefString("searchGroup") ?: ""
|
||||
val searchGroup = AppConfig.searchGroup
|
||||
bookSourceList.clear()
|
||||
searchBooks.clear()
|
||||
callBack?.onSearchSuccess(searchBooks)
|
||||
@@ -69,6 +68,7 @@ class SearchModel(private val scope: CoroutineScope) {
|
||||
} else {
|
||||
val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
|
||||
if (sources.isEmpty()) {
|
||||
AppConfig.searchGroup = ""
|
||||
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
|
||||
} else {
|
||||
bookSourceList.addAll(sources)
|
||||
|
||||
@@ -262,7 +262,7 @@ object WebBook {
|
||||
Debug.log(bookSource.bookSourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
|
||||
return bookChapter.url
|
||||
}
|
||||
if(bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {
|
||||
if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {
|
||||
Debug.log(bookSource.bookSourceUrl, "⇒一级目录正文不解析规则")
|
||||
return bookChapter.tag ?: ""
|
||||
}
|
||||
@@ -320,35 +320,38 @@ object WebBook {
|
||||
name: String,
|
||||
author: String,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
): Coroutine<Pair<BookSource, Book>> {
|
||||
): Coroutine<Pair<Book, BookSource>> {
|
||||
return Coroutine.async(scope, context) {
|
||||
preciseSearchAwait(scope, bookSources, name, author)
|
||||
?: throw NoStackTraceException("没有搜索到<$name>$author")
|
||||
for (source in bookSources) {
|
||||
val book = preciseSearchAwait(scope, source, name, author).getOrNull()
|
||||
if (book != null) {
|
||||
return@async Pair(book, source)
|
||||
}
|
||||
}
|
||||
throw NoStackTraceException("没有搜索到<$name>$author")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preciseSearchAwait(
|
||||
scope: CoroutineScope,
|
||||
bookSources: List<BookSource>,
|
||||
bookSource: BookSource,
|
||||
name: String,
|
||||
author: String
|
||||
): Pair<BookSource, Book>? {
|
||||
bookSources.forEach { source ->
|
||||
kotlin.runCatching {
|
||||
if (!scope.isActive) return null
|
||||
searchBookAwait(scope, source, name).firstOrNull {
|
||||
it.name == name && it.author == author
|
||||
}?.let { searchBook ->
|
||||
if (!scope.isActive) return null
|
||||
var book = searchBook.toBook()
|
||||
if (book.tocUrl.isBlank()) {
|
||||
book = getBookInfoAwait(scope, source, book)
|
||||
}
|
||||
return Pair(source, book)
|
||||
author: String,
|
||||
): Result<Book?> {
|
||||
return kotlin.runCatching {
|
||||
if (!scope.isActive) return@runCatching null
|
||||
searchBookAwait(scope, bookSource, name).firstOrNull {
|
||||
it.name == name && it.author == author
|
||||
}?.let { searchBook ->
|
||||
if (!scope.isActive) return@runCatching null
|
||||
var book = searchBook.toBook()
|
||||
if (book.tocUrl.isBlank()) {
|
||||
book = getBookInfoAwait(scope, bookSource, book)
|
||||
}
|
||||
return@runCatching book
|
||||
}
|
||||
return@runCatching null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,9 +31,8 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
||||
} else {
|
||||
DocumentFile.fromSingleUri(context, uri)?.readText(context)
|
||||
} ?: throw NoStackTraceException("文件不存在")
|
||||
if (content.isJson()) {
|
||||
//暂时根据文件内容判断属于什么
|
||||
when {
|
||||
when {
|
||||
content.isJson() -> when {
|
||||
content.contains("bookSourceUrl") ->
|
||||
importBookSourceLive.postValue(content)
|
||||
content.contains("sourceUrl") ->
|
||||
@@ -48,10 +47,12 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
||||
importHttpTTS(content, finally)
|
||||
else -> errorLiveData.postValue("格式不对")
|
||||
}
|
||||
} else if (uri.toString().matches(bookFileRegex)) {
|
||||
importBookLiveData.postValue(uri)
|
||||
} else {
|
||||
throw NoStackTraceException("暂未支持的本地书籍格式(TXT/UMD/EPUB)")
|
||||
(uri.path ?: uri.toString()).matches(bookFileRegex) -> {
|
||||
importBookLiveData.postValue(uri)
|
||||
}
|
||||
else -> {
|
||||
throw NoStackTraceException("暂未支持的本地书籍格式(TXT/UMD/EPUB)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onLineImportLive.postValue(uri)
|
||||
|
||||
@@ -189,7 +189,7 @@ class ImportBookSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_vie
|
||||
|
||||
override fun onCodeSave(code: String, requestId: String?) {
|
||||
requestId?.toInt()?.let {
|
||||
BookSource.fromJson(code)?.let { source ->
|
||||
BookSource.fromJson(code).getOrNull()?.let { source ->
|
||||
viewModel.allSources[it] = source
|
||||
adapter.setItem(it, source)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import io.legado.app.help.SourceHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.*
|
||||
|
||||
|
||||
@@ -98,13 +97,12 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
importSourceUrl(it)
|
||||
}
|
||||
} else {
|
||||
BookSource.fromJson(mText)?.let {
|
||||
BookSource.fromJson(mText).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
mText.isJsonArray() -> {
|
||||
val items = BookSource.fromJsonArray(mText)
|
||||
mText.isJsonArray() -> BookSource.fromJsonArray(mText).getOrThrow().let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
mText.isAbsUrl() -> {
|
||||
@@ -123,26 +121,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
private suspend fun importSourceUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text("utf-8").let { body ->
|
||||
when {
|
||||
body.isJsonArray() -> {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(body).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
BookSource.fromJson(jsonItem.jsonString())?.let { source ->
|
||||
allSources.add(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
body.isJsonObject() -> {
|
||||
BookSource.fromJson(body)?.let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
}
|
||||
}.byteStream().let {
|
||||
allSources.addAll(BookSource.fromJsonArray(it).getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,14 +92,14 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
|
||||
adapter.notifyDataSetChanged()
|
||||
upSelectText()
|
||||
}
|
||||
viewModel.errorLiveData.observe(this, {
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
})
|
||||
viewModel.successLiveData.observe(this, {
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allSources)
|
||||
@@ -110,7 +110,7 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
|
||||
visible()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
val source = arguments?.getString("source")
|
||||
if (source.isNullOrEmpty()) {
|
||||
dismiss()
|
||||
@@ -188,7 +188,7 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
|
||||
|
||||
override fun onCodeSave(code: String, requestId: String?) {
|
||||
requestId?.toInt()?.let {
|
||||
RssSource.fromJson(code)?.let { source ->
|
||||
RssSource.fromJson(code).getOrNull()?.let { source ->
|
||||
viewModel.allSources[it] = source
|
||||
adapter.setItem(it, source)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import io.legado.app.help.SourceHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.*
|
||||
|
||||
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
@@ -95,7 +94,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
importSourceUrl(it)
|
||||
}
|
||||
} else {
|
||||
RssSource.fromJsonArray(mText).let {
|
||||
RssSource.fromJsonArray(mText).getOrThrow().let {
|
||||
allSources.addAll(it)
|
||||
}
|
||||
}
|
||||
@@ -104,7 +103,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(mText).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
RssSource.fromJsonDoc(jsonItem)?.let {
|
||||
RssSource.fromJsonDoc(jsonItem).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
@@ -124,11 +123,11 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
|
||||
private suspend fun importSourceUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text("utf-8").let { body ->
|
||||
}.byteStream().let { body ->
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(body).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
RssSource.fromJson(jsonItem.jsonString())?.let { source ->
|
||||
RssSource.fromJson(jsonItem.jsonString()).getOrThrow().let { source ->
|
||||
allSources.add(source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.databinding.ActivityArrangeBookBinding
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.group.GroupManageDialog
|
||||
import io.legado.app.ui.book.group.GroupSelectDialog
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
import io.legado.app.ui.widget.SelectActionBar
|
||||
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
|
||||
import io.legado.app.ui.widget.recycler.ItemTouchCallback
|
||||
@@ -41,6 +43,7 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
|
||||
PopupMenu.OnMenuItemClickListener,
|
||||
SelectActionBar.CallBack,
|
||||
ArrangeBookAdapter.CallBack,
|
||||
SourcePickerDialog.Callback,
|
||||
GroupSelectDialog.CallBack {
|
||||
|
||||
override val binding by viewBinding(ActivityArrangeBookBinding::inflate)
|
||||
@@ -107,6 +110,17 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
|
||||
binding.selectActionBar.inflateMenu(R.menu.arrange_book_sel)
|
||||
binding.selectActionBar.setOnMenuItemClickListener(this)
|
||||
binding.selectActionBar.setCallBack(this)
|
||||
binding.composeView.setContent {
|
||||
AppTheme {
|
||||
BatchChangeSourceDialog(
|
||||
state = viewModel.batchChangeSourceState,
|
||||
size = viewModel.batchChangeSourceSize,
|
||||
position = viewModel.batchChangeSourcePosition
|
||||
) {
|
||||
viewModel.batchChangeSourceCoroutine?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@@ -169,6 +183,7 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
|
||||
R.id.menu_update_disable ->
|
||||
viewModel.upCanUpdate(adapter.selectedBooks(), false)
|
||||
R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0)
|
||||
R.id.menu_change_source -> showDialogFragment<SourcePickerDialog>()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -228,4 +243,9 @@ class ArrangeBookActivity : VMBaseActivity<ActivityArrangeBookBinding, ArrangeBo
|
||||
}
|
||||
}
|
||||
|
||||
override fun sourceOnClick(source: BookSource) {
|
||||
viewModel.changeSource(adapter.selectedBooks(), source)
|
||||
viewModel.batchChangeSourceState.value = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
package io.legado.app.ui.book.arrange
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
|
||||
|
||||
class ArrangeBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
val batchChangeSourceState = mutableStateOf(false)
|
||||
val batchChangeSourceSize = mutableStateOf(0)
|
||||
val batchChangeSourcePosition = mutableStateOf(0)
|
||||
var batchChangeSourceCoroutine: Coroutine<Unit>? = null
|
||||
|
||||
fun upCanUpdate(books: Array<Book>, canUpdate: Boolean) {
|
||||
execute {
|
||||
books.forEach {
|
||||
@@ -29,4 +38,24 @@ class ArrangeBookViewModel(application: Application) : BaseViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
fun changeSource(books: Array<Book>, source: BookSource) {
|
||||
batchChangeSourceCoroutine?.cancel()
|
||||
batchChangeSourceCoroutine = execute {
|
||||
batchChangeSourceSize.value = books.size
|
||||
books.forEachIndexed { index, book ->
|
||||
batchChangeSourcePosition.value = index + 1
|
||||
if (book.isLocalBook()) return@forEachIndexed
|
||||
if (book.origin == source.bookSourceUrl) return@forEachIndexed
|
||||
WebBook.preciseSearchAwait(this, source, book.name, book.author)
|
||||
.getOrNull()?.let { newBook ->
|
||||
val toc = WebBook.getChapterListAwait(this, source, newBook)
|
||||
book.changeTo(newBook, toc)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
batchChangeSourceState.value = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.legado.app.ui.book.arrange
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import io.legado.app.R
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Composable
|
||||
fun BatchChangeSourceDialog(
|
||||
state: MutableState<Boolean>,
|
||||
size: MutableState<Int>,
|
||||
position: MutableState<Int>,
|
||||
cancel: () -> Unit
|
||||
) {
|
||||
if (state.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
cancel.invoke()
|
||||
state.value = false
|
||||
}, content = {
|
||||
Text(text = "取消")
|
||||
})
|
||||
},
|
||||
title = {
|
||||
Text(text = appCtx.getString(R.string.change_source_batch))
|
||||
},
|
||||
text = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text = "${position.value}/${size.value}")
|
||||
LinearProgressIndicator(
|
||||
progress = position.value / size.value.toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package io.legado.app.ui.book.arrange
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseDialogFragment
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.databinding.DialogSourcePickerBinding
|
||||
import io.legado.app.databinding.Item1lineTextBinding
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.utils.applyTint
|
||||
import io.legado.app.utils.dpToPx
|
||||
import io.legado.app.utils.setLayout
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.views.onClick
|
||||
|
||||
class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) {
|
||||
|
||||
private val binding by viewBinding(DialogSourcePickerBinding::bind)
|
||||
private val searchView: SearchView by lazy {
|
||||
binding.toolBar.findViewById(R.id.search_view)
|
||||
}
|
||||
private val adapter by lazy {
|
||||
SourceAdapter(requireContext())
|
||||
}
|
||||
private var sourceFlowJob: Job? = null
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initData()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.toolBar.setBackgroundColor(primaryColor)
|
||||
binding.toolBar.title = "选择书源"
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
searchView.applyTint(primaryTextColor)
|
||||
searchView.onActionViewExpanded()
|
||||
searchView.isSubmitButtonEnabled = true
|
||||
searchView.queryHint = getString(R.string.search_book_source)
|
||||
searchView.clearFocus()
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
initData(newText)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initData(searchKey: String? = null) {
|
||||
sourceFlowJob?.cancel()
|
||||
sourceFlowJob = launch {
|
||||
when {
|
||||
searchKey.isNullOrEmpty() -> appDb.bookSourceDao.flowEnabled()
|
||||
else -> appDb.bookSourceDao.flowSearchEnabled(searchKey)
|
||||
}.collect {
|
||||
adapter.setItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class SourceAdapter(context: Context) :
|
||||
RecyclerAdapter<BookSource, Item1lineTextBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding {
|
||||
return Item1lineTextBinding.inflate(inflater, parent, false).apply {
|
||||
root.setPadding(16.dpToPx())
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: Item1lineTextBinding,
|
||||
item: BookSource,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.textView.text = item.getDisPlayNameGroup()
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) {
|
||||
binding.root.onClick {
|
||||
getItemByLayoutPosition(holder.layoutPosition)?.let {
|
||||
callback?.sourceOnClick(it)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val callback: Callback?
|
||||
get() {
|
||||
return (parentFragment as? Callback) ?: activity as? Callback
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun sourceOnClick(source: BookSource)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.Status
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.databinding.ActivityAudioPlayBinding
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
@@ -24,6 +26,7 @@ import io.legado.app.model.BookCover
|
||||
import io.legado.app.service.AudioPlayService
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.toc.TocActivityResult
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
@@ -31,6 +34,9 @@ import io.legado.app.ui.theme.AppTheme
|
||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.views.onLongClick
|
||||
import java.util.*
|
||||
|
||||
@@ -198,8 +204,21 @@ class AudioPlayActivity :
|
||||
override val oldBook: Book?
|
||||
get() = AudioPlay.book
|
||||
|
||||
override fun changeTo(source: BookSource, book: Book) {
|
||||
viewModel.changeTo(source, book)
|
||||
override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
if (book.type == BookType.audio) {
|
||||
viewModel.changeTo(source, book, toc)
|
||||
} else {
|
||||
AudioPlay.stop(this)
|
||||
launch {
|
||||
withContext(IO) {
|
||||
AudioPlay.book?.changeTo(book, toc)
|
||||
}
|
||||
startActivity<ReadBookActivity> {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
|
||||
@@ -9,8 +9,6 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.model.AudioPlay
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.utils.postEvent
|
||||
@@ -45,33 +43,23 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBookInfo(
|
||||
book: Book,
|
||||
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
|
||||
) {
|
||||
private fun loadBookInfo(book: Book) {
|
||||
execute {
|
||||
AudioPlay.bookSource?.let {
|
||||
WebBook.getBookInfo(this, it, book)
|
||||
.onSuccess {
|
||||
loadChapterList(book, changeDruChapterIndex)
|
||||
loadChapterList(book)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChapterList(
|
||||
book: Book,
|
||||
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
|
||||
) {
|
||||
private fun loadChapterList(book: Book) {
|
||||
execute {
|
||||
AudioPlay.bookSource?.let {
|
||||
WebBook.getChapterList(this, it, book)
|
||||
.onSuccess(Dispatchers.IO) { cList ->
|
||||
if (changeDruChapterIndex == null) {
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
} else {
|
||||
changeDruChapterIndex(cList)
|
||||
}
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
AudioPlay.upDurChapter(book)
|
||||
}.onError {
|
||||
context.toastOnUi(R.string.error_load_toc)
|
||||
@@ -88,47 +76,17 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTo(source: BookSource, book: Book) {
|
||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
execute {
|
||||
var oldTocSize: Int = book.totalChapterNum
|
||||
AudioPlay.book?.let {
|
||||
oldTocSize = it.totalChapterNum
|
||||
book.order = it.order
|
||||
appDb.bookDao.delete(it)
|
||||
}
|
||||
appDb.bookDao.insert(book)
|
||||
AudioPlay.book = book
|
||||
AudioPlay.book = AudioPlay.book!!.changeTo(book, toc)
|
||||
AudioPlay.bookSource = source
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book) { upChangeDurChapterIndex(book, oldTocSize, it) }
|
||||
} else {
|
||||
loadChapterList(book) { upChangeDurChapterIndex(book, oldTocSize, it) }
|
||||
}
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
AudioPlay.upDurChapter(book)
|
||||
}.onFinally {
|
||||
postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun upChangeDurChapterIndex(
|
||||
book: Book,
|
||||
oldTocSize: Int,
|
||||
chapters: List<BookChapter>
|
||||
) {
|
||||
execute {
|
||||
book.durChapterIndex = BookHelp.getDurChapter(
|
||||
book.durChapterIndex,
|
||||
book.durChapterTitle,
|
||||
chapters,
|
||||
oldTocSize
|
||||
)
|
||||
book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||
)
|
||||
appDb.bookDao.update(book)
|
||||
appDb.bookChapterDao.insert(*chapters.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromBookshelf(success: (() -> Unit)?) {
|
||||
execute {
|
||||
AudioPlay.book?.let {
|
||||
|
||||
@@ -32,7 +32,7 @@ fun TimerDialog(state: MutableState<Boolean>, parent: View) {
|
||||
timeMinute.value = it.toInt()
|
||||
AudioPlay.setTimer(it.toInt())
|
||||
},
|
||||
valueRange = 0f..180f
|
||||
valueRange = 0f..180f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.legado.app.ui.book.bookmark
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.databinding.ActivityAllBookmarkBinding
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
class AllBookmarkActivity : VMBaseActivity<ActivityAllBookmarkBinding, AllBookmarkViewModel>(),
|
||||
BookmarkAdapter.Callback,
|
||||
BookmarkDialog.Callback {
|
||||
|
||||
override val viewModel by viewModels<AllBookmarkViewModel>()
|
||||
override val binding by viewBinding(ActivityAllBookmarkBinding::inflate)
|
||||
private val adapter by lazy {
|
||||
BookmarkAdapter(this, this)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
viewModel.initData {
|
||||
adapter.setItems(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.recyclerView.addItemDecoration(BookmarkDecoration(adapter))
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onItemClick(bookmark: Bookmark, position: Int) {
|
||||
showDialogFragment(BookmarkDialog(bookmark, position))
|
||||
}
|
||||
|
||||
override fun upBookmark(pos: Int, bookmark: Bookmark) {
|
||||
adapter.setItem(pos, bookmark)
|
||||
}
|
||||
|
||||
override fun deleteBookmark(pos: Int) {
|
||||
adapter.getItem(pos)?.let {
|
||||
viewModel.deleteBookmark(it)
|
||||
}
|
||||
adapter.removeItem(pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.legado.app.ui.book.bookmark
|
||||
|
||||
import android.app.Application
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
|
||||
class AllBookmarkViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
|
||||
fun initData(onSuccess: (bookmarks: List<Bookmark>) -> Unit) {
|
||||
execute {
|
||||
appDb.bookmarkDao.all
|
||||
}.onSuccess {
|
||||
onSuccess.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteBookmark(bookmark: Bookmark) {
|
||||
execute {
|
||||
appDb.bookmarkDao.delete(bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.legado.app.ui.book.bookmark
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.databinding.ItemBookmarkBinding
|
||||
import io.legado.app.utils.gone
|
||||
import splitties.views.onClick
|
||||
|
||||
class BookmarkAdapter(context: Context, val callback: Callback) :
|
||||
RecyclerAdapter<Bookmark, ItemBookmarkBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemBookmarkBinding {
|
||||
return ItemBookmarkBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemBookmarkBinding,
|
||||
item: Bookmark,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.tvChapterName.text = item.chapterName
|
||||
binding.tvBookText.gone(item.bookText.isEmpty())
|
||||
binding.tvBookText.text = item.bookText
|
||||
binding.tvContent.gone(item.content.isEmpty())
|
||||
binding.tvContent.text = item.content
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) {
|
||||
binding.root.onClick {
|
||||
getItemByLayoutPosition(holder.layoutPosition)?.let {
|
||||
callback.onItemClick(it, holder.layoutPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getHeaderText(position: Int): String {
|
||||
return with(getItem(position)) {
|
||||
"${this?.bookName ?: ""}(${this?.bookAuthor ?: ""})"
|
||||
}
|
||||
}
|
||||
|
||||
fun isItemHeader(position: Int): Boolean {
|
||||
if (position == 0) return true
|
||||
val lastItem = getItem(position - 1)
|
||||
val curItem = getItem(position)
|
||||
return !(lastItem?.bookName == curItem?.bookName
|
||||
&& lastItem?.bookAuthor == curItem?.bookAuthor)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onItemClick(bookmark: Bookmark, position: Int)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package io.legado.app.ui.book.bookmark
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.text.TextPaint
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.backgroundColor
|
||||
import io.legado.app.utils.dpToPx
|
||||
import io.legado.app.utils.spToPx
|
||||
import splitties.init.appCtx
|
||||
import kotlin.math.min
|
||||
|
||||
class BookmarkDecoration(val adapter: BookmarkAdapter) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val headerLeft = 16f.dpToPx()
|
||||
private val headerHeight = 32f.dpToPx()
|
||||
|
||||
private val headerPaint = Paint().apply {
|
||||
color = appCtx.backgroundColor
|
||||
}
|
||||
private val textPaint = TextPaint().apply {
|
||||
textSize = 16f.spToPx()
|
||||
color = appCtx.accentColor
|
||||
}
|
||||
private val textRect = Rect()
|
||||
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val count = parent.childCount
|
||||
for (i in 0 until count) {
|
||||
val view = parent.getChildAt(i)
|
||||
val position = parent.getChildLayoutPosition(view)
|
||||
val isHeader = adapter.isItemHeader(position)
|
||||
if (isHeader) {
|
||||
c.drawRect(
|
||||
0f,
|
||||
view.top - headerHeight,
|
||||
parent.width.toFloat(),
|
||||
view.top.toFloat(),
|
||||
headerPaint
|
||||
)
|
||||
val headerText = adapter.getHeaderText(position)
|
||||
textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
|
||||
c.drawText(
|
||||
headerText,
|
||||
headerLeft,
|
||||
(view.top - headerHeight) + headerHeight / 2 + textRect.height() / 2,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val position = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
val view = parent.findViewHolderForAdapterPosition(position)?.itemView ?: return
|
||||
val isHeader = adapter.isItemHeader(position + 1)
|
||||
val headerText = adapter.getHeaderText(position)
|
||||
if (isHeader) {
|
||||
val bottom = min(headerHeight.toInt(), view.bottom)
|
||||
c.drawRect(
|
||||
0f,
|
||||
view.top - headerHeight,
|
||||
parent.width.toFloat(),
|
||||
bottom.toFloat(),
|
||||
headerPaint
|
||||
)
|
||||
textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
|
||||
c.drawText(
|
||||
headerText,
|
||||
headerLeft,
|
||||
headerHeight / 2 + textRect.height() / 2 - (headerHeight - bottom),
|
||||
textPaint
|
||||
)
|
||||
} else {
|
||||
c.drawRect(
|
||||
0f,
|
||||
0f,
|
||||
parent.width.toFloat(),
|
||||
headerHeight,
|
||||
headerPaint
|
||||
)
|
||||
textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
|
||||
c.drawText(
|
||||
headerText,
|
||||
headerLeft,
|
||||
headerHeight / 2 + textRect.height() / 2,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
c.save()
|
||||
}
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildLayoutPosition(view)
|
||||
val isHeader = adapter.isItemHeader(position)
|
||||
if (isHeader) {
|
||||
outRect.top = headerHeight.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.book.toc
|
||||
package io.legado.app.ui.book.bookmark
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -75,8 +75,9 @@ class BookmarkDialog() : BaseDialogFragment(R.layout.dialog_bookmark) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCallback(): Callback? {
|
||||
return parentFragment as? Callback
|
||||
private fun getCallback(): Callback? {
|
||||
return (parentFragment as? Callback)
|
||||
?: activity as? Callback
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
@@ -74,7 +74,9 @@ class ChangeBookSourceAdapter(
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.changeTo(it)
|
||||
if (it.bookUrl != callBack.bookUrl) {
|
||||
callBack.changeTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.itemView.onLongClick {
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.databinding.DialogBookChangeSourceBinding
|
||||
@@ -27,6 +28,7 @@ import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
@@ -50,6 +52,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
private val groups = linkedSetOf<String>()
|
||||
private val callBack: CallBack? get() = activity as? CallBack
|
||||
private val viewModel: ChangeBookSourceViewModel by viewModels()
|
||||
private val waitDialog by lazy { WaitDialog(requireContext()) }
|
||||
private val adapter by lazy { ChangeBookSourceAdapter(requireContext(), viewModel, this) }
|
||||
private val editSourceResult =
|
||||
registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
||||
@@ -57,14 +60,14 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
}
|
||||
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
|
||||
if (it) {
|
||||
val searchGroup = getPrefString("searchGroup")
|
||||
if (!searchGroup.isNullOrEmpty()) {
|
||||
val searchGroup = AppConfig.searchGroup
|
||||
if (searchGroup.isNotEmpty()) {
|
||||
launch {
|
||||
alert("搜索结果为空") {
|
||||
setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
|
||||
cancelButton()
|
||||
okButton {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
viewModel.startSearch()
|
||||
}
|
||||
}
|
||||
@@ -218,9 +221,9 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
if (item.title.toString() == getString(R.string.all_source)) {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
} else {
|
||||
putPrefString("searchGroup", item.title.toString())
|
||||
AppConfig.searchGroup = item.title.toString()
|
||||
}
|
||||
viewModel.startOrStopSearch()
|
||||
viewModel.refresh()
|
||||
@@ -241,8 +244,23 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
}
|
||||
|
||||
override fun changeTo(searchBook: SearchBook) {
|
||||
changeSource(searchBook)
|
||||
dismissAllowingStateLoss()
|
||||
if (searchBook.type == callBack?.oldBook?.type) {
|
||||
changeSource(searchBook) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
} else {
|
||||
alert(
|
||||
titleResource = R.string.book_type_different,
|
||||
messageResource = R.string.soure_change_source
|
||||
) {
|
||||
okButton {
|
||||
changeSource(searchBook) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val bookUrl: String?
|
||||
@@ -269,22 +287,23 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
override fun deleteSource(searchBook: SearchBook) {
|
||||
viewModel.del(searchBook)
|
||||
if (bookUrl == searchBook.bookUrl) {
|
||||
viewModel.firstSourceOrNull(searchBook)?.let {
|
||||
changeSource(it)
|
||||
viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source ->
|
||||
callBack?.changeTo(source, book, toc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeSource(searchBook: SearchBook) {
|
||||
try {
|
||||
val book = searchBook.toBook()
|
||||
book.upInfoFromOld(callBack?.oldBook)
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
callBack?.changeTo(source!!, book)
|
||||
searchBook.time = System.currentTimeMillis()
|
||||
viewModel.updateSource(searchBook)
|
||||
} catch (e: Exception) {
|
||||
toastOnUi("换源失败\n${e.localizedMessage}")
|
||||
private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) {
|
||||
waitDialog.setText(R.string.load_toc)
|
||||
waitDialog.show()
|
||||
val book = searchBook.toBook()
|
||||
viewModel.getToc(book, {
|
||||
waitDialog.dismiss()
|
||||
toastOnUi(it)
|
||||
}) { toc, source ->
|
||||
waitDialog.dismiss()
|
||||
callBack?.changeTo(source, book, toc)
|
||||
onSuccess?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +312,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
*/
|
||||
private fun upGroupMenu() {
|
||||
val menu: Menu = binding.toolBar.menu
|
||||
val selectedGroup = getPrefString("searchGroup")
|
||||
val selectedGroup = AppConfig.searchGroup
|
||||
menu.removeGroup(R.id.source_group)
|
||||
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
|
||||
var hasSelectedGroup = false
|
||||
@@ -325,7 +344,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
|
||||
|
||||
interface CallBack {
|
||||
val oldBook: Book?
|
||||
fun changeTo(source: BookSource, book: Book)
|
||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,14 +11,16 @@ import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
||||
@@ -27,8 +29,9 @@ import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import splitties.init.appCtx
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -44,7 +47,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
private var screenKey: String = ""
|
||||
private var bookSourceList = arrayListOf<BookSource>()
|
||||
private val searchBooks = Collections.synchronizedList(arrayListOf<SearchBook>())
|
||||
private val searchGroup get() = appCtx.getPrefString("searchGroup") ?: ""
|
||||
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
|
||||
private var searchCallback: SourceCallback? = null
|
||||
val searchDataFlow = callbackFlow {
|
||||
|
||||
@@ -86,6 +89,11 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
@Volatile
|
||||
private var searchIndex = -1
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
searchPool?.close()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun initData(arguments: Bundle?) {
|
||||
arguments?.let { bundle ->
|
||||
@@ -118,11 +126,13 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
appDb.searchBookDao.clear(name, author)
|
||||
searchBooks.clear()
|
||||
bookSourceList.clear()
|
||||
val searchGroup = AppConfig.searchGroup
|
||||
if (searchGroup.isBlank()) {
|
||||
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
|
||||
} else {
|
||||
val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
|
||||
if (sources.isEmpty()) {
|
||||
AppConfig.searchGroup = ""
|
||||
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
|
||||
} else {
|
||||
bookSourceList.addAll(sources)
|
||||
@@ -187,6 +197,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
|
||||
private suspend fun loadBookToc(scope: CoroutineScope, source: BookSource, book: Book) {
|
||||
val chapters = WebBook.getChapterListAwait(scope, source, book)
|
||||
tocMap[book.bookUrl] = chapters
|
||||
book.latestChapterTitle = chapters.last().title
|
||||
val searchBook: SearchBook = book.toSearchBook()
|
||||
searchCallback?.searchSuccess(searchBook)
|
||||
@@ -212,15 +223,23 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
private fun getDbSearchBooks(): List<SearchBook> {
|
||||
return if (screenKey.isEmpty()) {
|
||||
if (AppConfig.changeSourceCheckAuthor) {
|
||||
appDb.searchBookDao.getChangeSourceSearch(name, author, searchGroup)
|
||||
appDb.searchBookDao.getChangeSourceSearch(
|
||||
name, author, AppConfig.searchGroup
|
||||
)
|
||||
} else {
|
||||
appDb.searchBookDao.getChangeSourceSearch(name, "", searchGroup)
|
||||
appDb.searchBookDao.getChangeSourceSearch(
|
||||
name, "", AppConfig.searchGroup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (AppConfig.changeSourceCheckAuthor) {
|
||||
appDb.searchBookDao.getChangeSourceSearch(name, author, screenKey, searchGroup)
|
||||
appDb.searchBookDao.getChangeSourceSearch(
|
||||
name, author, screenKey, AppConfig.searchGroup
|
||||
)
|
||||
} else {
|
||||
appDb.searchBookDao.getChangeSourceSearch(name, "", screenKey, searchGroup)
|
||||
appDb.searchBookDao.getChangeSourceSearch(
|
||||
name, "", screenKey, AppConfig.searchGroup
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,9 +272,39 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
searchStateData.postValue(false)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
searchPool?.close()
|
||||
fun getToc(
|
||||
book: Book,
|
||||
onError: (msg: String) -> Unit,
|
||||
onSuccess: (toc: List<BookChapter>, source: BookSource) -> Unit
|
||||
) {
|
||||
execute {
|
||||
val toc = tocMap[book.bookUrl]
|
||||
if (toc != null) {
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
return@execute Pair(toc, source!!)
|
||||
}
|
||||
val result = getToc(book).getOrThrow()
|
||||
tocMap[book.bookUrl] = result.first
|
||||
return@execute result
|
||||
}.onSuccess {
|
||||
onSuccess.invoke(it.first, it.second)
|
||||
}.onError {
|
||||
onError.invoke(it.localizedMessage ?: "获取目录出错")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getToc(book: Book): Result<Pair<List<BookChapter>, BookSource>> {
|
||||
return kotlin.runCatching {
|
||||
withContext(IO) {
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw NoStackTraceException("书源不存在")
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(this, source, book)
|
||||
}
|
||||
val toc = WebBook.getChapterListAwait(this, source, book)
|
||||
Pair(toc, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableSource(searchBook: SearchBook) {
|
||||
@@ -310,8 +359,26 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
||||
searchCallback?.upAdapter()
|
||||
}
|
||||
|
||||
fun firstSourceOrNull(searchBook: SearchBook): SearchBook? {
|
||||
return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl }
|
||||
fun autoChangeSource(
|
||||
bookType: Int?,
|
||||
onSuccess: (book: Book, toc: List<BookChapter>, source: BookSource) -> Unit
|
||||
) {
|
||||
execute {
|
||||
searchBooks.forEach {
|
||||
if (it.type == bookType) {
|
||||
val book = it.toBook()
|
||||
val result = getToc(book).getOrNull()
|
||||
if (result != null) {
|
||||
return@execute Triple(book, result.first, result.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw NoStackTraceException("没有有效源")
|
||||
}.onSuccess {
|
||||
onSuccess.invoke(it.first, it.second, it.third)
|
||||
}.onError {
|
||||
context.toastOnUi("自动换源失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
interface SourceCallback {
|
||||
|
||||
@@ -67,13 +67,6 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
private val tocAdapter by lazy {
|
||||
ChangeChapterTocAdapter(requireContext(), this)
|
||||
}
|
||||
private val tocSuccess: (toc: List<BookChapter>) -> Unit = {
|
||||
tocAdapter.durChapterIndex =
|
||||
BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, it)
|
||||
binding.loadingToc.hide()
|
||||
tocAdapter.setItems(it)
|
||||
binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5)
|
||||
}
|
||||
private val contentSuccess: (content: String) -> Unit = {
|
||||
binding.loadingToc.hide()
|
||||
callBack?.replaceContent(it)
|
||||
@@ -82,14 +75,14 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
private var searchBook: SearchBook? = null
|
||||
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
|
||||
if (it) {
|
||||
val searchGroup = getPrefString("searchGroup")
|
||||
if (!searchGroup.isNullOrEmpty()) {
|
||||
val searchGroup = AppConfig.searchGroup
|
||||
if (searchGroup.isNotEmpty()) {
|
||||
launch {
|
||||
alert("搜索结果为空") {
|
||||
setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
|
||||
cancelButton()
|
||||
okButton {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
viewModel.startSearch()
|
||||
}
|
||||
}
|
||||
@@ -252,9 +245,9 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
if (item.title.toString() == getString(R.string.all_source)) {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
} else {
|
||||
putPrefString("searchGroup", item.title.toString())
|
||||
AppConfig.searchGroup = item.title.toString()
|
||||
}
|
||||
viewModel.startOrStopSearch()
|
||||
viewModel.refresh()
|
||||
@@ -279,9 +272,16 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
tocAdapter.setItems(null)
|
||||
binding.clToc.visible()
|
||||
binding.loadingToc.show()
|
||||
viewModel.getToc(searchBook, tocSuccess) {
|
||||
val book = searchBook.toBook()
|
||||
viewModel.getToc(book, {
|
||||
binding.clToc.gone()
|
||||
toastOnUi(it)
|
||||
}) { toc: List<BookChapter>, _: BookSource ->
|
||||
tocAdapter.durChapterIndex =
|
||||
BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, toc)
|
||||
binding.loadingToc.hide()
|
||||
tocAdapter.setItems(toc)
|
||||
binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,8 +309,8 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
override fun deleteSource(searchBook: SearchBook) {
|
||||
viewModel.del(searchBook)
|
||||
if (bookUrl == searchBook.bookUrl) {
|
||||
viewModel.firstSourceOrNull(searchBook)?.let {
|
||||
changeSource(it)
|
||||
viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source ->
|
||||
callBack?.changeTo(source, book, toc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,25 +326,12 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeSource(searchBook: SearchBook) {
|
||||
try {
|
||||
val book = searchBook.toBook()
|
||||
book.upInfoFromOld(callBack?.oldBook)
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
callBack?.changeTo(source!!, book)
|
||||
searchBook.time = System.currentTimeMillis()
|
||||
viewModel.updateSource(searchBook)
|
||||
} catch (e: Exception) {
|
||||
toastOnUi("换源失败\n${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分组菜单
|
||||
*/
|
||||
private fun upGroupMenu() {
|
||||
val menu: Menu = binding.toolBar.menu
|
||||
val selectedGroup = getPrefString("searchGroup")
|
||||
val selectedGroup = AppConfig.searchGroup
|
||||
menu.removeGroup(R.id.source_group)
|
||||
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
|
||||
var hasSelectedGroup = false
|
||||
@@ -388,7 +375,7 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
|
||||
|
||||
interface CallBack {
|
||||
val oldBook: Book?
|
||||
fun changeTo(source: BookSource, book: Book)
|
||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>)
|
||||
fun replaceContent(content: String)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,8 @@ import android.os.Bundle
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class ChangeChapterSourceViewModel(application: Application) :
|
||||
@@ -17,8 +15,6 @@ class ChangeChapterSourceViewModel(application: Application) :
|
||||
var chapterIndex: Int = 0
|
||||
var chapterTitle: String = ""
|
||||
|
||||
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
|
||||
|
||||
override fun initData(arguments: Bundle?) {
|
||||
super.initData(arguments)
|
||||
arguments?.let { bundle ->
|
||||
@@ -29,31 +25,6 @@ class ChangeChapterSourceViewModel(application: Application) :
|
||||
}
|
||||
}
|
||||
|
||||
fun getToc(
|
||||
searchBook: SearchBook,
|
||||
success: (toc: List<BookChapter>) -> Unit,
|
||||
error: (msg: String) -> Unit
|
||||
) {
|
||||
execute {
|
||||
return@execute tocMap[searchBook.bookUrl]
|
||||
?: let {
|
||||
val book = searchBook.toBook()
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw NoStackTraceException("书源不存在")
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(this, source, book)
|
||||
}
|
||||
val toc = WebBook.getChapterListAwait(this, source, book)
|
||||
tocMap[book.bookUrl] = toc
|
||||
toc
|
||||
}
|
||||
}.onSuccess {
|
||||
success(it)
|
||||
}.onError {
|
||||
error(it.localizedMessage ?: "获取目录出错")
|
||||
}
|
||||
}
|
||||
|
||||
fun getContent(
|
||||
book: Book,
|
||||
chapter: BookChapter,
|
||||
|
||||
@@ -457,9 +457,8 @@ class BookInfoActivity :
|
||||
override val oldBook: Book?
|
||||
get() = viewModel.bookData.value
|
||||
|
||||
override fun changeTo(source: BookSource, book: Book) {
|
||||
upLoading(true)
|
||||
viewModel.changeTo(source, book)
|
||||
override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
viewModel.changeTo(source, book, toc)
|
||||
}
|
||||
|
||||
override fun coverChangeTo(coverUrl: String) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.model.ReadBook
|
||||
@@ -24,7 +23,6 @@ import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ensureActive
|
||||
|
||||
class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
val bookData = MutableLiveData<Book>()
|
||||
@@ -107,12 +105,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
fun loadBookInfo(
|
||||
book: Book,
|
||||
canReName: Boolean = true,
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
|
||||
scope: CoroutineScope = viewModelScope
|
||||
) {
|
||||
execute(scope) {
|
||||
if (book.isLocalBook()) {
|
||||
loadChapter(book, scope, changeDruChapterIndex)
|
||||
loadChapter(book, scope)
|
||||
} else {
|
||||
bookSource?.let { bookSource ->
|
||||
WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
|
||||
@@ -121,7 +118,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
if (inBookshelf) {
|
||||
appDb.bookDao.update(book)
|
||||
}
|
||||
loadChapter(it, scope, changeDruChapterIndex)
|
||||
loadChapter(it, scope)
|
||||
}.onError {
|
||||
AppLog.put("获取数据信息失败\n${it.localizedMessage}", it)
|
||||
context.toastOnUi(R.string.error_get_book_info)
|
||||
@@ -136,8 +133,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
private fun loadChapter(
|
||||
book: Book,
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
|
||||
scope: CoroutineScope = viewModelScope
|
||||
) {
|
||||
execute(scope) {
|
||||
if (book.isLocalBook()) {
|
||||
@@ -156,11 +152,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*it.toTypedArray())
|
||||
}
|
||||
if (changeDruChapterIndex == null) {
|
||||
chapterListData.postValue(it)
|
||||
} else {
|
||||
changeDruChapterIndex(it)
|
||||
}
|
||||
chapterListData.postValue(it)
|
||||
}.onError {
|
||||
chapterListData.postValue(emptyList())
|
||||
AppLog.put("获取目录失败\n${it.localizedMessage}", it)
|
||||
@@ -184,58 +176,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTo(source: BookSource, newBook: Book) {
|
||||
fun changeTo(source: BookSource, newBook: Book, toc: List<BookChapter>) {
|
||||
changeSourceCoroutine?.cancel()
|
||||
changeSourceCoroutine = execute {
|
||||
var oldTocSize: Int = newBook.totalChapterNum
|
||||
if (inBookshelf) {
|
||||
bookData.value?.let {
|
||||
oldTocSize = it.totalChapterNum
|
||||
it.changeTo(newBook)
|
||||
}
|
||||
}
|
||||
bookData.postValue(newBook)
|
||||
bookSource = source
|
||||
if (newBook.tocUrl.isEmpty()) {
|
||||
loadBookInfo(newBook, false, this) {
|
||||
ensureActive()
|
||||
upChangeDurChapterIndex(newBook, oldTocSize, it)
|
||||
}
|
||||
} else {
|
||||
loadChapter(newBook, this) {
|
||||
ensureActive()
|
||||
upChangeDurChapterIndex(newBook, oldTocSize, it)
|
||||
}
|
||||
}
|
||||
bookData.value!!.changeTo(newBook, toc)
|
||||
bookData.postValue(newBook)
|
||||
chapterListData.postValue(toc)
|
||||
}.onFinally {
|
||||
postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun upChangeDurChapterIndex(
|
||||
book: Book,
|
||||
oldTocSize: Int,
|
||||
chapters: List<BookChapter>
|
||||
) {
|
||||
execute {
|
||||
book.durChapterIndex = BookHelp.getDurChapter(
|
||||
book.durChapterIndex,
|
||||
book.durChapterTitle,
|
||||
chapters,
|
||||
oldTocSize
|
||||
)
|
||||
book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||
)
|
||||
if (inBookshelf) {
|
||||
appDb.bookDao.update(book)
|
||||
appDb.bookChapterDao.insert(*chapters.toTypedArray())
|
||||
}
|
||||
bookData.postValue(book)
|
||||
chapterListData.postValue(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
fun topBook() {
|
||||
execute {
|
||||
bookData.value?.let { book ->
|
||||
@@ -300,7 +252,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
|
||||
execute {
|
||||
bookData.value?.let {
|
||||
Book.delete(it)
|
||||
it.delete()
|
||||
inBookshelf = false
|
||||
if (it.isLocalBook()) {
|
||||
LocalBook.deleteBook(it, deleteOriginal)
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.core.view.size
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Status
|
||||
@@ -38,6 +39,8 @@ import io.legado.app.model.ReadBook
|
||||
import io.legado.app.receiver.TimeBatteryReceiver
|
||||
import io.legado.app.service.BaseReadAloudService
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||
import io.legado.app.ui.book.bookmark.BookmarkDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeChapterSourceDialog
|
||||
import io.legado.app.ui.book.read.config.*
|
||||
@@ -51,7 +54,6 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
||||
import io.legado.app.ui.book.searchContent.SearchContentActivity
|
||||
import io.legado.app.ui.book.searchContent.SearchResult
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.toc.BookmarkDialog
|
||||
import io.legado.app.ui.book.toc.TocActivityResult
|
||||
import io.legado.app.ui.browser.WebViewActivity
|
||||
import io.legado.app.ui.dict.DictDialog
|
||||
@@ -715,8 +717,19 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
override val oldBook: Book?
|
||||
get() = ReadBook.book
|
||||
|
||||
override fun changeTo(source: BookSource, book: Book) {
|
||||
viewModel.changeTo(source, book)
|
||||
override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
if (book.type != BookType.audio) {
|
||||
viewModel.changeTo(source, book, toc)
|
||||
} else {
|
||||
ReadAloud.stop(this)
|
||||
launch {
|
||||
ReadBook.book?.changeTo(book, toc)
|
||||
}
|
||||
startActivity<AudioPlayActivity> {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceContent(content: String) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -30,7 +31,6 @@ import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.toStringArray
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ensureActive
|
||||
|
||||
class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
val permissionDenialLiveData = MutableLiveData<Int>()
|
||||
@@ -191,40 +191,22 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
/**
|
||||
* 换源
|
||||
*/
|
||||
fun changeTo(source: BookSource, book: Book) {
|
||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
changeSourceCoroutine?.cancel()
|
||||
changeSourceCoroutine = execute {
|
||||
ReadBook.upMsg(context.getString(R.string.loading))
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(this, source, book)
|
||||
}
|
||||
ensureActive()
|
||||
val chapters = WebBook.getChapterListAwait(this, source, book)
|
||||
ensureActive()
|
||||
val oldBook = ReadBook.book!!
|
||||
book.durChapterIndex = BookHelp.getDurChapter(
|
||||
oldBook.durChapterIndex,
|
||||
oldBook.durChapterTitle,
|
||||
chapters,
|
||||
oldBook.totalChapterNum
|
||||
)
|
||||
book.durChapterTitle = chapters[book.durChapterIndex].getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||
)
|
||||
ensureActive()
|
||||
val nextChapter = chapters.getOrElse(book.durChapterIndex) {
|
||||
chapters.first()
|
||||
ReadBook.book!!.changeTo(book, toc)
|
||||
val nextChapter = toc.getOrElse(book.durChapterIndex) {
|
||||
toc.first()
|
||||
}
|
||||
WebBook.getContentAwait(
|
||||
this,
|
||||
bookSource = source,
|
||||
book = book,
|
||||
bookChapter = chapters[book.durChapterIndex],
|
||||
bookChapter = toc[book.durChapterIndex],
|
||||
nextChapterUrl = nextChapter.url
|
||||
)
|
||||
ensureActive()
|
||||
oldBook.changeTo(book)
|
||||
appDb.bookChapterDao.insert(*chapters.toTypedArray())
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
ReadBook.resetData(book)
|
||||
ReadBook.upMsg(null)
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
@@ -244,10 +226,17 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
if (!AppConfig.autoChangeSource) return
|
||||
execute {
|
||||
val sources = appDb.bookSourceDao.allTextEnabled
|
||||
WebBook.preciseSearchAwait(this, sources, name, author)?.let {
|
||||
it.second.upInfoFromOld(ReadBook.book)
|
||||
changeTo(it.first, it.second)
|
||||
} ?: throw NoStackTraceException("自动换源失败")
|
||||
sources.forEach { source ->
|
||||
WebBook.preciseSearchAwait(this, source, name, author).getOrNull()?.let { book ->
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(this, source, book)
|
||||
}
|
||||
val toc = WebBook.getChapterListAwait(this, source, book)
|
||||
changeTo(source, book, toc)
|
||||
return@execute
|
||||
}
|
||||
}
|
||||
throw NoStackTraceException("自动换源失败")
|
||||
}.onStart {
|
||||
ReadBook.upMsg(context.getString(R.string.source_auto_changing))
|
||||
}.onError {
|
||||
@@ -272,7 +261,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
fun removeFromBookshelf(success: (() -> Unit)?) {
|
||||
execute {
|
||||
Book.delete(ReadBook.book)
|
||||
ReadBook.book?.delete()
|
||||
}.onSuccess {
|
||||
success?.invoke()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import io.legado.app.databinding.ActivityBookSearchBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.*
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
@@ -66,14 +67,14 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
||||
private var groups = linkedSetOf<String>()
|
||||
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
|
||||
if (it) {
|
||||
val searchGroup = getPrefString("searchGroup")
|
||||
if (!searchGroup.isNullOrEmpty()) {
|
||||
val searchGroup = AppConfig.searchGroup
|
||||
if (searchGroup.isNotEmpty()) {
|
||||
launch {
|
||||
alert("搜索结果为空") {
|
||||
setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
|
||||
cancelButton()
|
||||
okButton {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
viewModel.searchKey = ""
|
||||
viewModel.search(searchView.query.toString())
|
||||
}
|
||||
@@ -123,9 +124,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
||||
else -> if (item.groupId == R.id.source_group) {
|
||||
item.isChecked = true
|
||||
if (item.title.toString() == getString(R.string.all_source)) {
|
||||
putPrefString("searchGroup", "")
|
||||
AppConfig.searchGroup = ""
|
||||
} else {
|
||||
putPrefString("searchGroup", item.title.toString())
|
||||
AppConfig.searchGroup = item.title.toString()
|
||||
}
|
||||
searchView.query?.toString()?.trim()?.let {
|
||||
searchView.setQuery(it, true)
|
||||
@@ -280,7 +281,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
||||
* 更新分组菜单
|
||||
*/
|
||||
private fun upGroupMenu() = menu?.let { menu ->
|
||||
val selectedGroup = getPrefString("searchGroup")
|
||||
val selectedGroup = AppConfig.searchGroup
|
||||
menu.removeGroup(R.id.source_group)
|
||||
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
|
||||
var hasSelectedGroup = false
|
||||
|
||||
@@ -86,14 +86,12 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
|
||||
text.isJsonArray() -> {
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(text).read("$")
|
||||
val jsonItem = jsonPath.parse(items[0])
|
||||
BookSource.fromJson(jsonItem.jsonString())
|
||||
BookSource.fromJson(jsonItem.jsonString()).getOrThrow()
|
||||
}
|
||||
text.isJsonObject() -> {
|
||||
BookSource.fromJson(text)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
BookSource.fromJson(text).getOrThrow()
|
||||
}
|
||||
else -> throw NoStackTraceException("格式不对")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,10 +230,10 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
|
||||
}
|
||||
searchKey.startsWith("group:") -> {
|
||||
val key = searchKey.substringAfter("group:")
|
||||
appDb.bookSourceDao.flowGroupSearch("%$key%")
|
||||
appDb.bookSourceDao.flowGroupSearch(key)
|
||||
}
|
||||
else -> {
|
||||
appDb.bookSourceDao.flowSearch("%$searchKey%")
|
||||
appDb.bookSourceDao.flowSearch(searchKey)
|
||||
}
|
||||
}.conflate().map { data ->
|
||||
if (sortAscending) when (sort) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.databinding.ItemBookmarkBinding
|
||||
import io.legado.app.utils.gone
|
||||
import splitties.views.onLongClick
|
||||
|
||||
class BookmarkAdapter(context: Context, val callback: Callback) :
|
||||
@@ -22,7 +23,9 @@ class BookmarkAdapter(context: Context, val callback: Callback) :
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.tvChapterName.text = item.chapterName
|
||||
binding.tvBookText.gone(item.bookText.isEmpty())
|
||||
binding.tvBookText.text = item.bookText
|
||||
binding.tvContent.gone(item.content.isEmpty())
|
||||
binding.tvContent.text = item.content
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.databinding.FragmentBookmarkBinding
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.bookmark.BookmarkDialog
|
||||
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
|
||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
||||
import io.legado.app.utils.setEdgeEffectColor
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.legado.app.databinding.ActivityMainBinding
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.storage.Backup
|
||||
import io.legado.app.lib.theme.elevation
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
@@ -175,7 +176,9 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
BookHelp.clearRemovedCache()
|
||||
Coroutine.async {
|
||||
BookHelp.clearInvalidCache()
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeLiveBus() {
|
||||
|
||||
@@ -132,7 +132,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
||||
if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
|
||||
WebBook.preciseSearch(this, bookSources, name, author)
|
||||
.onSuccess {
|
||||
val book = it.second
|
||||
val book = it.first
|
||||
if (groupId > 0) {
|
||||
book.group = groupId
|
||||
}
|
||||
|
||||
@@ -123,10 +123,10 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
|
||||
}
|
||||
searchKey.startsWith("group:") -> {
|
||||
val key = searchKey.substringAfter("group:")
|
||||
appDb.bookSourceDao.flowGroupExplore("%$key%")
|
||||
appDb.bookSourceDao.flowGroupExplore(key)
|
||||
}
|
||||
else -> {
|
||||
appDb.bookSourceDao.flowExplore("%$searchKey%")
|
||||
appDb.bookSourceDao.flowExplore(searchKey)
|
||||
}
|
||||
}.catch {
|
||||
AppLog.put("发现界面更新数据出错", it)
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.legado.app.service.WebService
|
||||
import io.legado.app.ui.about.AboutActivity
|
||||
import io.legado.app.ui.about.DonateActivity
|
||||
import io.legado.app.ui.about.ReadRecordActivity
|
||||
import io.legado.app.ui.book.bookmark.AllBookmarkActivity
|
||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||
import io.legado.app.ui.config.ConfigActivity
|
||||
import io.legado.app.ui.config.ConfigTag
|
||||
@@ -135,6 +136,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
||||
when (preference.key) {
|
||||
"bookSourceManage" -> startActivity<BookSourceActivity>()
|
||||
"replaceManage" -> startActivity<ReplaceRuleActivity>()
|
||||
"bookmark" -> startActivity<AllBookmarkActivity>()
|
||||
"setting" -> startActivity<ConfigActivity> {
|
||||
putExtra("configTag", ConfigTag.OTHER_CONFIG)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
|
||||
execute(context = Dispatchers.Main) {
|
||||
var source: RssSource? = null
|
||||
context.getClipText()?.let { json ->
|
||||
source = RssSource.fromJson(json)
|
||||
source = RssSource.fromJson(json).getOrThrow()
|
||||
}
|
||||
source
|
||||
}.onError {
|
||||
@@ -69,7 +69,7 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
|
||||
fun importSource(text: String, finally: (source: RssSource) -> Unit) {
|
||||
execute {
|
||||
val text1 = text.trim()
|
||||
RssSource.fromJson(text1)?.let {
|
||||
RssSource.fromJson(text1).getOrThrow().let {
|
||||
finally.invoke(it)
|
||||
}
|
||||
}.onError {
|
||||
|
||||
@@ -6,23 +6,36 @@ import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import splitties.init.appCtx
|
||||
|
||||
object AppTheme {
|
||||
|
||||
val colors
|
||||
get() = if (ThemeConfig.isDarkTheme()) {
|
||||
darkColors(
|
||||
primary = Color(appCtx.accentColor),
|
||||
primaryVariant = Color(ColorUtils.darkenColor(appCtx.accentColor)),
|
||||
secondary = Color(appCtx.primaryColor),
|
||||
secondaryVariant = Color(appCtx.primaryColor)
|
||||
)
|
||||
} else {
|
||||
lightColors(
|
||||
primary = Color(appCtx.accentColor),
|
||||
primaryVariant = Color(ColorUtils.darkenColor(appCtx.accentColor)),
|
||||
secondary = Color(appCtx.primaryColor),
|
||||
secondaryVariant = Color(appCtx.primaryColor)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppTheme(content: @Composable () -> Unit) {
|
||||
val colors = if (ThemeConfig.isDarkTheme()) {
|
||||
darkColors(
|
||||
primary = Color(appCtx.primaryColor),
|
||||
)
|
||||
} else {
|
||||
lightColors(
|
||||
primary = Color(appCtx.primaryColor),
|
||||
)
|
||||
}
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
colors = AppTheme.colors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -98,6 +98,11 @@ class SelectActionBar @JvmOverloads constructor(
|
||||
btnRevertSelection.isClickable = isClickable
|
||||
btnSelectActionMain.isEnabled = isClickable
|
||||
btnSelectActionMain.isClickable = isClickable
|
||||
if (isClickable) {
|
||||
ivMenuMore.setColorFilter(context.primaryTextColor)
|
||||
} else {
|
||||
ivMenuMore.setColorFilter(context.secondaryTextColor)
|
||||
}
|
||||
ivMenuMore.isEnabled = isClickable
|
||||
ivMenuMore.isClickable = isClickable
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.google.gson.*
|
||||
import com.google.gson.internal.LinkedTreeMap
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.lang.reflect.ParameterizedType
|
||||
@@ -37,6 +39,20 @@ inline fun <reified T> Gson.fromJsonArray(json: String?): Result<List<T>?> {
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Gson.fromJsonObject(inputStream: InputStream?): Result<T?> {
|
||||
return kotlin.runCatching {
|
||||
val reader = InputStreamReader(inputStream)
|
||||
fromJson(reader, genericType<T>()) as? T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Gson.fromJsonArray(inputStream: InputStream?): Result<List<T>?> {
|
||||
return kotlin.runCatching {
|
||||
val reader = InputStreamReader(inputStream)
|
||||
fromJson(reader, ParameterizedTypeImpl(T::class.java)) as? List<T>
|
||||
}
|
||||
}
|
||||
|
||||
fun Gson.writeToOutputStream(out: OutputStream, any: Any) {
|
||||
val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
|
||||
writer.setIndent(" ")
|
||||
|
||||
@@ -98,6 +98,14 @@ fun View.gone() {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.gone(gone: Boolean) {
|
||||
if (gone) {
|
||||
gone()
|
||||
} else {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.invisible() {
|
||||
if (visibility != INVISIBLE) {
|
||||
visibility = INVISIBLE
|
||||
|
||||
Reference in New Issue
Block a user