同步上游更新

This commit is contained in:
HapeLee
2025-11-26 23:24:23 +08:00
parent d9fbd730c3
commit 15a25eface
39 changed files with 301 additions and 340 deletions
@@ -21,6 +21,7 @@ object EventBus {
const val NOTIFY_MAIN = "notifyMain" const val NOTIFY_MAIN = "notifyMain"
const val WEB_SERVICE = "webService" const val WEB_SERVICE = "webService"
const val UP_DOWNLOAD = "upDownload" const val UP_DOWNLOAD = "upDownload"
const val UP_DOWNLOAD_STATE = "upDownloadState"
const val SAVE_CONTENT = "saveContent" const val SAVE_CONTENT = "saveContent"
const val CHECK_SOURCE = "checkSource" const val CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone" const val CHECK_SOURCE_DONE = "checkSourceDone"
@@ -55,7 +55,8 @@ data class HttpTTS(
loginUrl = doc.readString("$.loginUrl"), loginUrl = doc.readString("$.loginUrl"),
loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(), loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(),
header = doc.readString("$.header"), header = doc.readString("$.header"),
loginCheckJs = doc.readString("$.loginCheckJs") loginCheckJs = doc.readString("$.loginCheckJs"),
lastUpdateTime = doc.readLong("$.lastUpdateTime") ?: System.currentTimeMillis()
) )
} }
} }
@@ -16,8 +16,8 @@ class ReadRecordRepository(
) { ) {
companion object { companion object {
// 2分钟内重新打开书本,视为同一次阅读 // 10秒内重新打开书本,视为同一次阅读
private const val CONTINUE_THRESHOLD = 120 * 1000L private const val CONTINUE_THRESHOLD = 10 * 1000L
// 会话总时长小于10秒则不记录 // 会话总时长小于10秒则不记录
private const val MIN_READ_DURATION = 0 * 1000L private const val MIN_READ_DURATION = 0 * 1000L
} }
@@ -1,3 +1,3 @@
package io.legado.app.help.glide.progress package io.legado.app.help.glide.progress
typealias OnProgressListener = ((isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) -> Unit)? typealias OnProgressListener = (isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) -> Unit
@@ -28,7 +28,7 @@ object ProgressManager {
} }
fun addListener(url: String, listener: OnProgressListener) { fun addListener(url: String, listener: OnProgressListener) {
if (url.isNotEmpty() && listener != null) { if (url.isNotEmpty()) {
val url = getUrlNoOption(url) val url = getUrlNoOption(url)
listenersMap[url] = listener listenersMap[url] = listener
listener.invoke(false, 1, 0, 0) listener.invoke(false, 1, 0, 0)
@@ -42,7 +42,7 @@ object ProgressManager {
} }
} }
fun getProgressListener(url: String): OnProgressListener { fun getProgressListener(url: String): OnProgressListener? {
return if (url.isEmpty() || listenersMap.isEmpty()) { return if (url.isEmpty() || listenersMap.isEmpty()) {
null null
} else { } else {
@@ -53,7 +53,7 @@ object ProgressManager {
private fun getUrlNoOption(url: String): String { private fun getUrlNoOption(url: String): String {
val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) val urlMatcher = AnalyzeUrl.paramPattern.matcher(url)
return if (urlMatcher.find()) { return if (urlMatcher.find()) {
url.substring(0, urlMatcher.start()) url.take(urlMatcher.start())
} else { } else {
url url
} }
@@ -11,25 +11,39 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.exception.ConcurrentException import io.legado.app.exception.ConcurrentException
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService import io.legado.app.service.CacheBookService
import io.legado.app.utils.onEachParallel
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.startService import io.legado.app.utils.startService
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
object CacheBook { object CacheBook {
val cacheBookMap = ConcurrentHashMap<String, CacheBookModel>() val cacheBookMap = ConcurrentHashMap<String, CacheBookModel>()
private val workingState = MutableStateFlow(true)
private val mutex = Mutex()
val totalCount: Int val totalCount: Int
get() { get() {
var total = 0 var total = 0
@@ -117,6 +131,39 @@ object CacheBook {
errorDownloadMap.clear() errorDownloadMap.clear()
} }
fun setWorkingState(value: Boolean) {
workingState.value = value
}
suspend fun startProcessJob(context: CoroutineContext) = mutex.withLock {
setWorkingState(true)
flow {
while (currentCoroutineContext().isActive && cacheBookMap.isNotEmpty()) {
var emitted = false
cacheBookMap.forEach { (_, model) ->
if (!model.isLoading()) {
emit(model)
emitted = true
}
workingState.first { it }
}
if (!emitted) {
delay(1000)
}
}
}.onStart {
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
}.onEachParallel(AppConfig.threadCount) {
coroutineScope {
it.download(this, context)
}
}.onCompletion {
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
}.collect()
}
val downloadSummary: String val downloadSummary: String
get() { get() {
return "正在下载:${onDownloadCount}|等待中:${waitCount}|失败:${errorDownloadMap.count()}|成功:${successDownloadSet.size}" return "正在下载:${onDownloadCount}|等待中:${waitCount}|失败:${errorDownloadMap.count()}|成功:${successDownloadSet.size}"
@@ -124,11 +171,12 @@ object CacheBook {
val isRun: Boolean val isRun: Boolean
get() { get() {
var isRun = false
cacheBookMap.forEach { cacheBookMap.forEach {
isRun = isRun || it.value.isRun() if (it.value.isRun()) {
return true
}
} }
return isRun return false
} }
private val waitCount: Int private val waitCount: Int
@@ -159,6 +207,7 @@ object CacheBook {
private val tasks = CompositeCoroutine() private val tasks = CompositeCoroutine()
private var isStopped = false private var isStopped = false
private var waitingRetry = false private var waitingRetry = false
private var isLoading = false
val waitCount get() = waitDownloadSet.size val waitCount get() = waitDownloadSet.size
val onDownloadCount get() = onDownloadSet.size val onDownloadCount get() = onDownloadSet.size
@@ -169,7 +218,7 @@ object CacheBook {
@Synchronized @Synchronized
fun isRun(): Boolean { fun isRun(): Boolean {
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading
} }
@Synchronized @Synchronized
@@ -177,11 +226,22 @@ object CacheBook {
return isStopped || (!isRun() && !waitingRetry) return isStopped || (!isRun() && !waitingRetry)
} }
@Synchronized
fun isLoading(): Boolean {
return isLoading
}
@Synchronized
fun setLoading() {
isLoading = true
}
@Synchronized @Synchronized
fun stop() { fun stop() {
waitDownloadSet.clear() waitDownloadSet.clear()
tasks.clear() tasks.clear()
isStopped = true isStopped = true
isLoading = false
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
} }
@@ -194,6 +254,7 @@ object CacheBook {
} }
} }
cacheBookMap[book.bookUrl] = this cacheBookMap[book.bookUrl] = this
isLoading = false
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
} }
@@ -257,10 +318,9 @@ object CacheBook {
*/ */
@Synchronized @Synchronized
fun download(scope: CoroutineScope, context: CoroutineContext) { fun download(scope: CoroutineScope, context: CoroutineContext) {
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
val chapterIndex = waitDownloadSet.firstOrNull() val chapterIndex = waitDownloadSet.firstOrNull()
if (chapterIndex == null) { if (chapterIndex == null) {
if (onDownloadSet.isEmpty()) { if (!isLoading && onDownloadSet.isEmpty()) {
cacheBookMap.remove(book.bookUrl) cacheBookMap.remove(book.bookUrl)
} }
return return
@@ -331,7 +391,6 @@ object CacheBook {
} }
suspend fun downloadAwait(chapter: BookChapter): String { suspend fun downloadAwait(chapter: BookChapter): String {
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
synchronized(this) { synchronized(this) {
onDownloadSet.add(chapter.index) onDownloadSet.add(chapter.index)
waitDownloadSet.remove(chapter.index) waitDownloadSet.remove(chapter.index)
@@ -365,7 +424,6 @@ object CacheBook {
if (onDownloadSet.contains(chapter.index)) { if (onDownloadSet.contains(chapter.index)) {
return return
} }
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
onDownloadSet.add(chapter.index) onDownloadSet.add(chapter.index)
waitDownloadSet.remove(chapter.index) waitDownloadSet.remove(chapter.index)
WebBook.getContent( WebBook.getContent(
@@ -371,7 +371,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
return return
} }
try { try {
readRecordRepository.saveOrMergeReadSession(sessionToSave) readRecordRepository.saveReadSession(sessionToSave)
} catch (e: Exception) { } catch (e: Exception) {
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e) AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
} finally { } finally {
@@ -217,7 +217,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
return return
} }
try { try {
readRecordRepository.saveOrMergeReadSession(sessionToSave) readRecordRepository.saveReadSession(sessionToSave)
} catch (e: Exception) { } catch (e: Exception) {
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e) AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
} finally { } finally {
@@ -121,12 +121,14 @@ class CacheBookService : BaseService() {
val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl) val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl)
if (chapterCount == 0) { if (chapterCount == 0) {
cacheBook.setLoading()
mutex.withLock { mutex.withLock {
val name = book.name val name = book.name
if (book.tocUrl.isEmpty()) { if (book.tocUrl.isEmpty()) {
kotlin.runCatching { kotlin.runCatching {
WebBook.getBookInfoAwait(cacheBook.bookSource, book) WebBook.getBookInfoAwait(cacheBook.bookSource, book)
}.onFailure { }.onFailure {
removeDownload(bookUrl)
AppLog.put( AppLog.put(
"$name》目录为空且加载详情页失败\n${it.localizedMessage}", "$name》目录为空且加载详情页失败\n${it.localizedMessage}",
it, it,
@@ -141,6 +143,7 @@ class CacheBookService : BaseService() {
book.totalChapterNum = 0 book.totalChapterNum = 0
book.update() book.update()
} }
removeDownload(bookUrl)
AppLog.put( AppLog.put(
"$name》目录为空且加载目录失败\n${it.localizedMessage}", "$name》目录为空且加载目录失败\n${it.localizedMessage}",
it, it,
@@ -189,23 +192,8 @@ class CacheBookService : BaseService() {
private fun download() { private fun download() {
downloadJob?.cancel() downloadJob?.cancel()
downloadJob = lifecycleScope.launch(cachePool) { downloadJob = lifecycleScope.launch(cachePool) {
while (isActive) { CacheBook.startProcessJob(cachePool)
if (!CacheBook.isRun) { stopSelf()
stopSelf()
return@launch
}
CacheBook.cacheBookMap.forEach {
val cacheBookModel = it.value
while (cacheBookModel.waitCount > 0) {
if (CacheBook.onDownloadCount < threadCount) {
cacheBookModel.download(this, cachePool)
} else {
delay(100)
}
}
}
delay(100)
}
} }
} }
@@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.utils.contains import io.legado.app.utils.contains
import io.legado.app.utils.inputStream import io.legado.app.utils.inputStream
import io.legado.app.utils.jsonPath
import io.legado.app.utils.readText import io.legado.app.utils.readText
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) { abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
@@ -14,28 +15,33 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
val errorLive = MutableLiveData<String>() val errorLive = MutableLiveData<String>()
fun importJson(uri: Uri) { fun importJson(uri: Uri) {
when { val map = uri.inputStream(context).getOrThrow().use {
uri.inputStream(context).getOrNull().contains("bookSourceUrl") -> jsonPath.parse(it).read<Map<String, *>>("$[0]")
successLive.postValue(Pair("bookSource", uri.toString())) } ?: uri.inputStream(context).getOrThrow().use {
else -> importJson(uri.readText(context)) jsonPath.parse(it).read("$")
} }
}
private fun importJson(json: String) {
//暂时根据文件内容判断属于什么
when { when {
json.contains("sourceUrl") -> map.containsKey("bookSourceUrl") ->
successLive.postValue(Pair("rssSource", json)) successLive.postValue("bookSource" to uri.toString())
json.contains("pattern") ->
successLive.postValue(Pair("replaceRule", json)) map.containsKey("sourceUrl") ->
json.contains("themeName") -> successLive.postValue("rssSource" to uri.toString())
successLive.postValue(Pair("theme", json))
json.contains("urlRule") && json.contains("showRule") -> map.containsKey("pattern") ->
successLive.postValue(Pair("dictRule", json)) successLive.postValue("replaceRule" to uri.toString())
json.contains("name") && json.contains("rule") ->
successLive.postValue(Pair("txtRule", json)) map.containsKey("themeName") ->
json.contains("name") && json.contains("url") -> successLive.postValue("theme" to uri.toString())
successLive.postValue(Pair("httpTts", json))
map.containsKey("showRule") ->
successLive.postValue("dictRule" to uri.toString())
map.containsKey("name") && map.containsKey("rule") ->
successLive.postValue("txtRule" to uri.toString())
map.containsKey("name") && map.containsKey("url") ->
successLive.postValue("httpTts" to uri.toString())
else -> errorLive.postValue("格式不对") else -> errorLive.postValue("格式不对")
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
@@ -19,6 +20,9 @@ import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.readText
import splitties.init.appCtx
class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) { class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) {
@@ -88,6 +92,9 @@ class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> { text.isAbsUrl() -> {
importSourceUrl(text) importSourceUrl(text)
} }
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
@@ -16,6 +17,9 @@ import io.legado.app.help.http.text
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.readText
import splitties.init.appCtx
class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) { class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
@@ -85,6 +89,9 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> { text.isAbsUrl() -> {
importSourceUrl(text) importSourceUrl(text)
} }
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
@@ -17,7 +18,10 @@ import io.legado.app.help.http.text
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.readText
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import splitties.init.appCtx
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false var isAddGroup = false
@@ -102,6 +106,10 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
allRules.add(rule) allRules.add(rule)
} }
text.isUri() -> {
importAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException("格式不对") else -> throw NoStackTraceException("格式不对")
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.R import io.legado.app.R
@@ -22,8 +23,11 @@ import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.jsonPath import io.legado.app.utils.jsonPath
import io.legado.app.utils.readText
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import splitties.init.appCtx
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false var isAddGroup = false
@@ -101,42 +105,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSource(text: String) { fun importSource(text: String) {
execute { execute {
val mText = text.trim() importSourceAwait(text)
when {
mText.isJsonObject() -> kotlin.runCatching {
val json = JsonPath.parse(mText)
val urls = json.read<List<String>>("$.sourceUrls")
if (!urls.isNullOrEmpty()) {
urls.forEach {
importSourceUrl(it)
}
}
}.onFailure {
GSON.fromJsonArray<RssSource>(mText).getOrThrow().let {
val source = it.firstOrNull() ?: return@let
if (source.sourceUrl.isEmpty()) {
throw NoStackTraceException("不是订阅源")
}
allSources.addAll(it)
}
}
mText.isJsonArray() -> {
GSON.fromJsonArray<RssSource>(mText).getOrThrow().let {
val source = it.firstOrNull() ?: return@let
if (source.sourceUrl.isEmpty()) {
throw NoStackTraceException("不是订阅源")
}
allSources.addAll(it)
}
}
mText.isAbsUrl() -> {
importSourceUrl(mText)
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}.onError { }.onError {
errorLiveData.postValue("ImportError:${it.localizedMessage}") errorLiveData.postValue("ImportError:${it.localizedMessage}")
AppLog.put("ImportError:${it.localizedMessage}", it) AppLog.put("ImportError:${it.localizedMessage}", it)
@@ -145,6 +114,53 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
} }
} }
private suspend fun importSourceAwait(text: String) {
val mText = text.trim()
when {
mText.isJsonObject() -> kotlin.runCatching {
val json = JsonPath.parse(mText)
val urls = json.read<List<String>>("$.sourceUrls")
if (!urls.isNullOrEmpty()) {
urls.forEach {
importSourceUrl(it)
}
}
}.onFailure {
GSON.fromJsonArray<RssSource>(mText).getOrThrow().let {
val source = it.firstOrNull() ?: return@let
if (source.sourceUrl.isEmpty()) {
throw NoStackTraceException("不是订阅源")
}
allSources.addAll(it)
}
}
mText.isJsonArray() -> {
GSON.fromJsonArray<RssSource>(mText).getOrThrow().let {
val source = it.firstOrNull() ?: return@let
if (source.sourceUrl.isEmpty()) {
throw NoStackTraceException("不是订阅源")
}
allSources.addAll(it)
}
}
mText.isAbsUrl() -> {
importSourceUrl(mText)
}
mText.isUri() -> {
importSourceAwait(mText.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}
private suspend fun importSourceUrl(url: String) { private suspend fun importSourceUrl(url: String) {
okHttpClient.newCallResponseBody { okHttpClient.newCallResponseBody {
if (url.endsWith("#requestWithoutUA")) { if (url.endsWith("#requestWithoutUA")) {
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
@@ -13,6 +14,7 @@ import io.legado.app.help.http.newCallResponseBody
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text import io.legado.app.help.http.text
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx
class ImportThemeViewModel(app: Application) : BaseViewModel(app) { class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
@@ -81,6 +83,9 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> { text.isAbsUrl() -> {
importSourceUrl(text) importSourceUrl(text)
} }
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
@@ -19,6 +20,9 @@ import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.readText
import splitties.init.appCtx
class ImportTxtTocRuleViewModel(app: Application) : BaseViewModel(app) { class ImportTxtTocRuleViewModel(app: Application) : BaseViewModel(app) {
@@ -89,6 +93,9 @@ class ImportTxtTocRuleViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> { text.isAbsUrl() -> {
importSourceUrl(text) importSourceUrl(text)
} }
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
} }
} }
@@ -299,6 +299,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
notifyItemChanged(it) notifyItemChanged(it)
} }
observeEvent<String>(EventBus.UP_DOWNLOAD) { observeEvent<String>(EventBus.UP_DOWNLOAD) {
notifyItemChanged(it)
}
observeEvent<String>(EventBus.UP_DOWNLOAD_STATE) {
if (!CacheBook.isRun) { if (!CacheBook.isRun) {
menu?.findItem(R.id.menu_download)?.let { item -> menu?.findItem(R.id.menu_download)?.let { item ->
item.setIcon(R.drawable.ic_play) item.setIcon(R.drawable.ic_play)
@@ -312,7 +315,6 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
menu?.applyTint(this) menu?.applyTint(this)
} }
notifyItemChanged(it)
} }
observeEvent<Pair<Book, BookChapter>>(EventBus.SAVE_CONTENT) { (book, chapter) -> observeEvent<Pair<Book, BookChapter>>(EventBus.SAVE_CONTENT) { (book, chapter) ->
viewModel.cacheChapters[book.bookUrl]?.add(chapter.url) viewModel.cacheChapters[book.bookUrl]?.add(chapter.url)
@@ -48,20 +48,6 @@ abstract class BaseImportBookActivity<VM : ViewModel> :
initSearchView() initSearchView()
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
/** /**
* 设置书籍保存位置 * 设置书籍保存位置
*/ */
@@ -34,7 +34,8 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t
private val webDavServerUi = listOf( private val webDavServerUi = listOf(
RowUi("url"), RowUi("url"),
RowUi("username"), RowUi("username"),
RowUi("password", RowUi.Type.password) RowUi("password", RowUi.Type.password),
RowUi("ipRange")
) )
override fun onStart() { override fun onStart() {
@@ -118,20 +118,6 @@ class BookshelfManageActivity :
upBookDataByGroupId() upBookDataByGroupId()
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun observeLiveBus() { override fun observeLiveBus() {
viewModel.batchChangeSourceState.observe(this) { viewModel.batchChangeSourceState.observe(this) {
if (it) { if (it) {
@@ -57,7 +57,6 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
} }
} }
binding.btnPaddingSetting.setOnClickListener { binding.btnPaddingSetting.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showPaddingConfig() callBack?.showPaddingConfig()
} }
upTvTipColor() upTvTipColor()
@@ -101,12 +101,10 @@ class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_
} }
tvTextFont.setOnClickListener { tvTextFont.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showFont() callBack?.showFont()
} }
tvPadding.setOnClickListener { tvPadding.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showInfoConfig() callBack?.showInfoConfig()
} }
tvTip.setOnClickListener { tvTip.setOnClickListener {
@@ -168,7 +166,6 @@ class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_
} }
private fun showBgTextConfig(index: Int): Boolean { private fun showBgTextConfig(index: Int): Boolean {
dismissAllowingStateLoss()
changeBgTextConfig(index) changeBgTextConfig(index)
callBack?.showBgTextConfig() callBack?.showBgTextConfig()
return true return true
@@ -63,7 +63,7 @@ class SpeakEngineDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
private val callBack: CallBack? get() = parentFragment as? CallBack private val callBack: CallBack? get() = parentFragment as? CallBack
private val importDocResult = registerForActivityResult(HandleFileContract()) { private val importDocResult = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri -> it.uri?.let { uri ->
viewModel.importLocal(uri) showDialogFragment(ImportHttpTtsDialog(uri.toString()))
} }
} }
private val exportDirResult = registerForActivityResult(HandleFileContract()) { private val exportDirResult = registerForActivityResult(HandleFileContract()) {
@@ -1,17 +1,9 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.app.Application import android.app.Application
import android.net.Uri
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.readText
import io.legado.app.utils.toastOnUi
class SpeakEngineViewModel(application: Application) : BaseViewModel(application) { class SpeakEngineViewModel(application: Application) : BaseViewModel(application) {
@@ -28,34 +20,4 @@ class SpeakEngineViewModel(application: Application) : BaseViewModel(application
} }
} }
fun importLocal(uri: Uri) {
execute {
import(uri.readText(context))
}.onSuccess {
context.toastOnUi("导入成功")
}.onError {
context.toastOnUi("导入失败\n${it.localizedMessage}")
}
}
fun import(text: String) {
when {
text.isJsonArray() -> {
HttpTTS.fromJsonArray(text).getOrThrow().let {
appDb.httpTTSDao.insert(*it.toTypedArray())
}
}
text.isJsonObject() -> {
HttpTTS.fromJson(text).getOrThrow().let {
appDb.httpTTSDao.insert(it)
}
}
else -> {
throw NoStackTraceException("格式不对")
}
}
}
} }
@@ -16,7 +16,7 @@ class FadePageDelegate(readView: ReadView) : PageDelegate(readView) {
private val slopSquare get() = readView.pageSlopSquare2 private val slopSquare get() = readView.pageSlopSquare2
private var fadeProgress = 0f private var fadeProgress = 0f
private val flipThreshold = 0.2f private val flipThreshold = 0.1f
override fun setDirection(direction: PageDirection) { override fun setDirection(direction: PageDirection) {
super.setDirection(direction) super.setDirection(direction)
@@ -95,20 +95,6 @@ class SearchContentActivity :
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
private fun initSearchResultList(list: List<SearchResult>?, position: Int) { private fun initSearchResultList(list: List<SearchResult>?, position: Int) {
list ?: return list ?: return
viewModel.searchResultList.addAll(list) viewModel.searchResultList.addAll(list)
@@ -122,30 +122,37 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
} }
initExploreKinds() initExploreKinds()
} }
@SuppressLint("SetTextI18n")
private fun initExploreKinds() { private fun initExploreKinds() {
lifecycleScope.launch { lifecycleScope.launch {
val exploreKinds = viewModel.bookSource?.exploreKinds()?.filter { try {
!it.url.isNullOrBlank() val exploreKinds = viewModel.bookSource?.exploreKinds()?.filter {
} !it.url.isNullOrBlank()
exploreKinds?.firstOrNull()?.let {
binding.textFx.text = "${it.title}::${it.url}"
if (it.title.startsWith("ERROR:")) {
adapter.addItem("获取发现出错\n${it.url}")
openOrCloseHelp(false)
searchView.clearFocus()
return@launch
} }
} exploreKinds?.firstOrNull()?.let {
@Suppress("USELESS_ELVIS") binding.textFx.text = "${it.title}::${it.url}"
exploreKinds?.map { it.title ?: "" }?.let { exploreKindTitles -> if (it.title.startsWith("ERROR:")) {
binding.textFx.onLongClick { adapter.addItem("获取发现出错\n${it.url}")
selector("选择发现", exploreKindTitles) { _, index -> openOrCloseHelp(false)
val explore = exploreKinds[index] searchView.clearFocus()
binding.textFx.text = "${explore.title}::${explore.url}" return@launch
searchView.setQuery(binding.textFx.text, true)
} }
} }
@Suppress("USELESS_ELVIS")
exploreKinds?.map { it.title ?: "" }?.let { exploreKindTitles ->
binding.textFx.onLongClick {
selector("选择发现", exploreKindTitles) { _, index ->
val explore = exploreKinds[index]
binding.textFx.text = "${explore.title}::${explore.url}"
searchView.setQuery(binding.textFx.text, true)
}
}
}
} catch (e: NullPointerException) {
adapter.addItem("获取发现出错 JSON 数据错误\n$e")
openOrCloseHelp(false)
searchView.clearFocus()
} }
} }
} }
@@ -632,6 +632,8 @@ class BookSourceEditActivity :
val edit = view.editableText//获取EditText的文字 val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) { if (start < 0 || start >= edit.length) {
edit.append(text) edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else { } else {
edit.replace(start, end, text)//光标所在位置插入文字 edit.replace(start, end, text)//光标所在位置插入文字
} }
@@ -158,20 +158,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.book_source, menu) menuInflater.inflate(R.menu.book_source, menu)
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
@@ -198,17 +198,6 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
} }
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.hideSoftInput()
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.book_toc, menu) menuInflater.inflate(R.menu.book_toc, menu)
this.menu = menu this.menu = menu
@@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import androidx.core.view.setPadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.script.rhino.runScriptWithContext import com.script.rhino.runScriptWithContext
@@ -20,6 +21,7 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.about.AppLogDialog import io.legado.app.ui.about.AppLogDialog
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.dpToPx
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
@@ -47,47 +49,51 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
binding.toolBar.title = getString(R.string.login_source, source.getTag()) binding.toolBar.title = getString(R.string.login_source, source.getTag())
val loginInfo = source.getLoginInfoMap() val loginInfo = source.getLoginInfoMap()
val loginUi = source.loginUi() val loginUi = source.loginUi()
loginUi?.forEachIndexed { index, rowUi -> try {
when (rowUi.type) { loginUi?.forEachIndexed { index, rowUi ->
RowUi.Type.text -> ItemSourceEditBinding.inflate( when (rowUi.type) {
layoutInflater, RowUi.Type.text -> ItemSourceEditBinding.inflate(
binding.root, layoutInflater,
false binding.root,
).let { false
binding.flexbox.addView(it.root) ).let {
it.root.id = index + 1000 binding.flexbox.addView(it.root)
it.textInputLayout.hint = rowUi.name it.root.id = index + 1000
it.editText.setText(loginInfo?.get(rowUi.name)) it.textInputLayout.hint = rowUi.name
} it.editText.setText(loginInfo?.get(rowUi.name))
}
RowUi.Type.password -> ItemSourceEditBinding.inflate( RowUi.Type.password -> ItemSourceEditBinding.inflate(
layoutInflater, layoutInflater,
binding.root, binding.root,
false false
).let { ).let {
binding.flexbox.addView(it.root) binding.flexbox.addView(it.root)
it.root.id = index + 1000 it.root.id = index + 1000
it.textInputLayout.hint = rowUi.name it.textInputLayout.hint = rowUi.name
it.editText.inputType = it.editText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
it.editText.setText(loginInfo?.get(rowUi.name)) it.editText.setText(loginInfo?.get(rowUi.name))
} }
RowUi.Type.button -> ItemFilletTextBinding.inflate( RowUi.Type.button -> ItemFilletTextBinding.inflate(
layoutInflater, layoutInflater,
binding.root, binding.root,
false false
).let { ).let {
binding.flexbox.addView(it.root) binding.flexbox.addView(it.root)
rowUi.style().apply(it.root) rowUi.style().apply(it.root)
it.root.id = index + 1000 it.root.id = index + 1000
it.textView.text = rowUi.name it.textView.text = rowUi.name
it.textView.setPadding(16.dpToPx())
it.root.onClick { it.root.onClick {
handleButtonClick(source, rowUi, loginUi) handleButtonClick(source, rowUi, loginUi)
}
} }
} }
} }
} catch (e: NullPointerException) {
AppLog.put("登录UI JSON 数据错误", e, true)
} }
binding.toolBar.inflateMenu(R.menu.source_login) binding.toolBar.inflateMenu(R.menu.source_login)
binding.toolBar.menu.applyTint(requireContext()) binding.toolBar.menu.applyTint(requireContext())
@@ -147,20 +147,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
toggleSystemBar(AppConfig.showStatusBar) toggleSystemBar(AppConfig.showStatusBar)
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
@@ -213,28 +213,14 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
if (AppConfig.preDownloadNum == 0) return if (AppConfig.preDownloadNum == 0) return
cacheBookJob?.cancel() cacheBookJob?.cancel()
cacheBookJob = viewModelScope.launch(upTocPool) { cacheBookJob = viewModelScope.launch(upTocPool) {
while (isActive) { launch {
if (CacheBookService.isRun || !CacheBook.isRun) { while (isActive && CacheBook.isRun) {
cacheBookJob?.cancel() //有目录更新是不缓存,优先更新目录,现在更多网站限制并发
cacheBookJob = null CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty())
return@launch delay(1000)
} }
CacheBook.cacheBookMap.forEach {
val cacheBookModel = it.value
while (cacheBookModel.waitCount > 0) {
//有目录更新是不缓存,优先更新目录,现在更多网站限制并发
if (waitUpTocBooks.isEmpty()
&& onUpTocBooks.isEmpty()
&& CacheBook.onDownloadCount < threadCount
) {
cacheBookModel.download(this, upTocPool)
} else {
delay(100)
}
}
}
delay(100)
} }
CacheBook.startProcessJob(upTocPool)
} }
} }
@@ -116,20 +116,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
observeGroupData() observeGroupData()
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.replace_rule, menu) menuInflater.inflate(R.menu.replace_rule, menu)
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
@@ -203,6 +203,8 @@ class ReplaceEditActivity :
val edit = view.editableText val edit = view.editableText
if (start < 0 || start >= edit.length) { if (start < 0 || start >= edit.length) {
edit.append(text) edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else { } else {
edit.replace(start, end, text) edit.replace(start, end, text)
} }
@@ -396,6 +396,8 @@ class RssSourceEditActivity :
val edit = view.editableText//获取EditText的文字 val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) { if (start < 0 || start >= edit.length) {
edit.append(text) edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else { } else {
edit.replace(start, end, text)//光标所在位置插入文字 edit.replace(start, end, text)//光标所在位置插入文字
} }
@@ -114,20 +114,6 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
initSelectActionBar() initSelectActionBar()
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.rss_source, menu) menuInflater.inflate(R.menu.rss_source, menu)
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
@@ -5,6 +5,7 @@ import androidx.lifecycle.repeatOnLifecycle
import io.legado.app.data.appDb import io.legado.app.data.appDb
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
@@ -43,7 +44,7 @@ inline fun <T> Flow<T>.onEachParallelSafe(
try { try {
action(value) action(value)
} catch (e: Throwable) { } catch (e: Throwable) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
} }
emit(value) emit(value)
} }
@@ -65,7 +66,7 @@ inline fun <T, R> Flow<T>.mapParallelSafe(
try { try {
emit(transform(value)) emit(transform(value))
} catch (_: Throwable) { } catch (_: Throwable) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
} }
} }
}.buffer(0) }.buffer(0)
@@ -79,7 +80,7 @@ inline fun <T, R> Flow<T>.transformParallelSafe(
try { try {
transform(value) transform(value)
} catch (e: Throwable) { } catch (e: Throwable) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
} }
} }
}.buffer(0) }.buffer(0)
@@ -1,31 +1,31 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="16dp"> android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:elevation="5dp"
android:theme="?attr/actionBarStyle"
app:title="@string/server_config"
app:displayHomeAsUp="false"
app:fitStatusBar="false" />
<LinearLayout <LinearLayout
android:id="@+id/vw_bg" android:id="@+id/vw_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/shape_card_view"
android:orientation="vertical" android:orientation="vertical"
tools:ignore="UselessParent"> tools:ignore="UselessParent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:elevation="5dp"
android:theme="?attr/actionBarStyle"
app:title="@string/server_config"
app:displayHomeAsUp="false"
app:fitStatusBar="false" />
<io.legado.app.ui.widget.text.TextInputLayout <io.legado.app.ui.widget.text.TextInputLayout
android:id="@+id/til_name" android:id="@+id/til_name"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -73,7 +73,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:clipToPadding="false" android:clipToPadding="false"
android:padding="3dp"
app:dividerDrawable="@drawable/shape_space_divider" app:dividerDrawable="@drawable/shape_space_divider"
app:flexDirection="row" app:flexDirection="row"
app:flexWrap="wrap" app:flexWrap="wrap"
@@ -82,4 +81,4 @@
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</LinearLayout> </LinearLayout>
</FrameLayout> </LinearLayout>