同步上游更新

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 WEB_SERVICE = "webService"
const val UP_DOWNLOAD = "upDownload"
const val UP_DOWNLOAD_STATE = "upDownloadState"
const val SAVE_CONTENT = "saveContent"
const val CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone"
@@ -55,7 +55,8 @@ data class HttpTTS(
loginUrl = doc.readString("$.loginUrl"),
loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(),
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 {
// 2分钟内重新打开书本,视为同一次阅读
private const val CONTINUE_THRESHOLD = 120 * 1000L
// 10秒内重新打开书本,视为同一次阅读
private const val CONTINUE_THRESHOLD = 10 * 1000L
// 会话总时长小于10秒则不记录
private const val MIN_READ_DURATION = 0 * 1000L
}
@@ -1,3 +1,3 @@
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) {
if (url.isNotEmpty() && listener != null) {
if (url.isNotEmpty()) {
val url = getUrlNoOption(url)
listenersMap[url] = listener
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()) {
null
} else {
@@ -53,7 +53,7 @@ object ProgressManager {
private fun getUrlNoOption(url: String): String {
val urlMatcher = AnalyzeUrl.paramPattern.matcher(url)
return if (urlMatcher.find()) {
url.substring(0, urlMatcher.start())
url.take(urlMatcher.start())
} else {
url
}
@@ -11,25 +11,39 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.exception.ConcurrentException
import io.legado.app.help.book.BookHelp
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.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService
import io.legado.app.utils.onEachParallel
import io.legado.app.utils.postEvent
import io.legado.app.utils.startService
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
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.withLock
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext
object CacheBook {
val cacheBookMap = ConcurrentHashMap<String, CacheBookModel>()
private val workingState = MutableStateFlow(true)
private val mutex = Mutex()
val totalCount: Int
get() {
var total = 0
@@ -117,6 +131,39 @@ object CacheBook {
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
get() {
return "正在下载:${onDownloadCount}|等待中:${waitCount}|失败:${errorDownloadMap.count()}|成功:${successDownloadSet.size}"
@@ -124,11 +171,12 @@ object CacheBook {
val isRun: Boolean
get() {
var isRun = false
cacheBookMap.forEach {
isRun = isRun || it.value.isRun()
if (it.value.isRun()) {
return true
}
}
return isRun
return false
}
private val waitCount: Int
@@ -159,6 +207,7 @@ object CacheBook {
private val tasks = CompositeCoroutine()
private var isStopped = false
private var waitingRetry = false
private var isLoading = false
val waitCount get() = waitDownloadSet.size
val onDownloadCount get() = onDownloadSet.size
@@ -169,7 +218,7 @@ object CacheBook {
@Synchronized
fun isRun(): Boolean {
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty()
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading
}
@Synchronized
@@ -177,11 +226,22 @@ object CacheBook {
return isStopped || (!isRun() && !waitingRetry)
}
@Synchronized
fun isLoading(): Boolean {
return isLoading
}
@Synchronized
fun setLoading() {
isLoading = true
}
@Synchronized
fun stop() {
waitDownloadSet.clear()
tasks.clear()
isStopped = true
isLoading = false
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
}
@@ -194,6 +254,7 @@ object CacheBook {
}
}
cacheBookMap[book.bookUrl] = this
isLoading = false
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
}
@@ -257,10 +318,9 @@ object CacheBook {
*/
@Synchronized
fun download(scope: CoroutineScope, context: CoroutineContext) {
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
val chapterIndex = waitDownloadSet.firstOrNull()
if (chapterIndex == null) {
if (onDownloadSet.isEmpty()) {
if (!isLoading && onDownloadSet.isEmpty()) {
cacheBookMap.remove(book.bookUrl)
}
return
@@ -331,7 +391,6 @@ object CacheBook {
}
suspend fun downloadAwait(chapter: BookChapter): String {
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
synchronized(this) {
onDownloadSet.add(chapter.index)
waitDownloadSet.remove(chapter.index)
@@ -365,7 +424,6 @@ object CacheBook {
if (onDownloadSet.contains(chapter.index)) {
return
}
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
onDownloadSet.add(chapter.index)
waitDownloadSet.remove(chapter.index)
WebBook.getContent(
@@ -371,7 +371,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
return
}
try {
readRecordRepository.saveOrMergeReadSession(sessionToSave)
readRecordRepository.saveReadSession(sessionToSave)
} catch (e: Exception) {
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
} finally {
@@ -217,7 +217,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
return
}
try {
readRecordRepository.saveOrMergeReadSession(sessionToSave)
readRecordRepository.saveReadSession(sessionToSave)
} catch (e: Exception) {
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
} finally {
@@ -121,12 +121,14 @@ class CacheBookService : BaseService() {
val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl)
if (chapterCount == 0) {
cacheBook.setLoading()
mutex.withLock {
val name = book.name
if (book.tocUrl.isEmpty()) {
kotlin.runCatching {
WebBook.getBookInfoAwait(cacheBook.bookSource, book)
}.onFailure {
removeDownload(bookUrl)
AppLog.put(
"$name》目录为空且加载详情页失败\n${it.localizedMessage}",
it,
@@ -141,6 +143,7 @@ class CacheBookService : BaseService() {
book.totalChapterNum = 0
book.update()
}
removeDownload(bookUrl)
AppLog.put(
"$name》目录为空且加载目录失败\n${it.localizedMessage}",
it,
@@ -189,23 +192,8 @@ class CacheBookService : BaseService() {
private fun download() {
downloadJob?.cancel()
downloadJob = lifecycleScope.launch(cachePool) {
while (isActive) {
if (!CacheBook.isRun) {
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)
}
CacheBook.startProcessJob(cachePool)
stopSelf()
}
}
@@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.utils.contains
import io.legado.app.utils.inputStream
import io.legado.app.utils.jsonPath
import io.legado.app.utils.readText
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
@@ -14,28 +15,33 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
val errorLive = MutableLiveData<String>()
fun importJson(uri: Uri) {
when {
uri.inputStream(context).getOrNull().contains("bookSourceUrl") ->
successLive.postValue(Pair("bookSource", uri.toString()))
else -> importJson(uri.readText(context))
val map = uri.inputStream(context).getOrThrow().use {
jsonPath.parse(it).read<Map<String, *>>("$[0]")
} ?: uri.inputStream(context).getOrThrow().use {
jsonPath.parse(it).read("$")
}
}
private fun importJson(json: String) {
//暂时根据文件内容判断属于什么
when {
json.contains("sourceUrl") ->
successLive.postValue(Pair("rssSource", json))
json.contains("pattern") ->
successLive.postValue(Pair("replaceRule", json))
json.contains("themeName") ->
successLive.postValue(Pair("theme", json))
json.contains("urlRule") && json.contains("showRule") ->
successLive.postValue(Pair("dictRule", json))
json.contains("name") && json.contains("rule") ->
successLive.postValue(Pair("txtRule", json))
json.contains("name") && json.contains("url") ->
successLive.postValue(Pair("httpTts", json))
map.containsKey("bookSourceUrl") ->
successLive.postValue("bookSource" to uri.toString())
map.containsKey("sourceUrl") ->
successLive.postValue("rssSource" to uri.toString())
map.containsKey("pattern") ->
successLive.postValue("replaceRule" to uri.toString())
map.containsKey("themeName") ->
successLive.postValue("theme" to uri.toString())
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("格式不对")
}
}
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import io.legado.app.R
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.isJsonArray
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) {
@@ -88,6 +92,9 @@ class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> {
importSourceUrl(text)
}
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import io.legado.app.R
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.isJsonArray
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) {
@@ -85,6 +89,9 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> {
importSourceUrl(text)
}
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
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.isJsonArray
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 splitties.init.appCtx
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
@@ -102,6 +106,10 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
allRules.add(rule)
}
text.isUri() -> {
importAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException("格式不对")
}
}
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath
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.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isUri
import io.legado.app.utils.jsonPath
import io.legado.app.utils.readText
import io.legado.app.utils.splitNotBlank
import splitties.init.appCtx
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
@@ -101,42 +105,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSource(text: String) {
execute {
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)
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
importSourceAwait(text)
}.onError {
errorLiveData.postValue("ImportError:${it.localizedMessage}")
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) {
okHttpClient.newCallResponseBody {
if (url.endsWith("#requestWithoutUA")) {
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import io.legado.app.R
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.text
import io.legado.app.utils.*
import splitties.init.appCtx
class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
@@ -81,6 +83,9 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> {
importSourceUrl(text)
}
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}
@@ -1,6 +1,7 @@
package io.legado.app.ui.association
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import io.legado.app.R
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.isJsonArray
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) {
@@ -89,6 +93,9 @@ class ImportTxtTocRuleViewModel(app: Application) : BaseViewModel(app) {
text.isAbsUrl() -> {
importSourceUrl(text)
}
text.isUri() -> {
importSourceAwait(text.toUri().readText(appCtx))
}
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
}
}
@@ -299,6 +299,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
notifyItemChanged(it)
}
observeEvent<String>(EventBus.UP_DOWNLOAD) {
notifyItemChanged(it)
}
observeEvent<String>(EventBus.UP_DOWNLOAD_STATE) {
if (!CacheBook.isRun) {
menu?.findItem(R.id.menu_download)?.let { item ->
item.setIcon(R.drawable.ic_play)
@@ -312,7 +315,6 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}
menu?.applyTint(this)
}
notifyItemChanged(it)
}
observeEvent<Pair<Book, BookChapter>>(EventBus.SAVE_CONTENT) { (book, chapter) ->
viewModel.cacheChapters[book.bookUrl]?.add(chapter.url)
@@ -48,20 +48,6 @@ abstract class BaseImportBookActivity<VM : ViewModel> :
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(
RowUi("url"),
RowUi("username"),
RowUi("password", RowUi.Type.password)
RowUi("password", RowUi.Type.password),
RowUi("ipRange")
)
override fun onStart() {
@@ -118,20 +118,6 @@ class BookshelfManageActivity :
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() {
viewModel.batchChangeSourceState.observe(this) {
if (it) {
@@ -57,7 +57,6 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
}
}
binding.btnPaddingSetting.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showPaddingConfig()
}
upTvTipColor()
@@ -101,12 +101,10 @@ class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_
}
tvTextFont.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showFont()
}
tvPadding.setOnClickListener {
dismissAllowingStateLoss()
callBack?.showInfoConfig()
}
tvTip.setOnClickListener {
@@ -168,7 +166,6 @@ class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_
}
private fun showBgTextConfig(index: Int): Boolean {
dismissAllowingStateLoss()
changeBgTextConfig(index)
callBack?.showBgTextConfig()
return true
@@ -63,7 +63,7 @@ class SpeakEngineDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
private val callBack: CallBack? get() = parentFragment as? CallBack
private val importDocResult = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
viewModel.importLocal(uri)
showDialogFragment(ImportHttpTtsDialog(uri.toString()))
}
}
private val exportDirResult = registerForActivityResult(HandleFileContract()) {
@@ -1,17 +1,9 @@
package io.legado.app.ui.book.read.config
import android.app.Application
import android.net.Uri
import android.speech.tts.TextToSpeech
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.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) {
@@ -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 var fadeProgress = 0f
private val flipThreshold = 0.2f
private val flipThreshold = 0.1f
override fun setDirection(direction: PageDirection) {
super.setDirection(direction)
@@ -95,20 +95,6 @@ class SearchContentActivity :
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) {
list ?: return
viewModel.searchResultList.addAll(list)
@@ -122,30 +122,37 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
}
initExploreKinds()
}
@SuppressLint("SetTextI18n")
private fun initExploreKinds() {
lifecycleScope.launch {
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
try {
val exploreKinds = viewModel.bookSource?.exploreKinds()?.filter {
!it.url.isNullOrBlank()
}
}
@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)
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
}
}
@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的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else {
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 {
menuInflater.inflate(R.menu.book_source, 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 {
menuInflater.inflate(R.menu.book_toc, menu)
this.menu = menu
@@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.os.Bundle
import android.text.InputType
import android.view.View
import androidx.core.view.setPadding
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
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.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.dpToPx
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.openUrl
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())
val loginInfo = source.getLoginInfoMap()
val loginUi = source.loginUi()
loginUi?.forEachIndexed { index, rowUi ->
when (rowUi.type) {
RowUi.Type.text -> ItemSourceEditBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
it.root.id = index + 1000
it.textInputLayout.hint = rowUi.name
it.editText.setText(loginInfo?.get(rowUi.name))
}
try {
loginUi?.forEachIndexed { index, rowUi ->
when (rowUi.type) {
RowUi.Type.text -> ItemSourceEditBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
it.root.id = index + 1000
it.textInputLayout.hint = rowUi.name
it.editText.setText(loginInfo?.get(rowUi.name))
}
RowUi.Type.password -> ItemSourceEditBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
it.root.id = index + 1000
it.textInputLayout.hint = rowUi.name
it.editText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
it.editText.setText(loginInfo?.get(rowUi.name))
}
RowUi.Type.password -> ItemSourceEditBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
it.root.id = index + 1000
it.textInputLayout.hint = rowUi.name
it.editText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
it.editText.setText(loginInfo?.get(rowUi.name))
}
RowUi.Type.button -> ItemFilletTextBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
rowUi.style().apply(it.root)
it.root.id = index + 1000
it.textView.text = rowUi.name
it.root.onClick {
handleButtonClick(source, rowUi, loginUi)
RowUi.Type.button -> ItemFilletTextBinding.inflate(
layoutInflater,
binding.root,
false
).let {
binding.flexbox.addView(it.root)
rowUi.style().apply(it.root)
it.root.id = index + 1000
it.textView.text = rowUi.name
it.textView.setPadding(16.dpToPx())
it.root.onClick {
handleButtonClick(source, rowUi, loginUi)
}
}
}
}
} catch (e: NullPointerException) {
AppLog.put("登录UI JSON 数据错误", e, true)
}
binding.toolBar.inflateMenu(R.menu.source_login)
binding.toolBar.menu.applyTint(requireContext())
@@ -147,20 +147,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
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?) {
super.onPostCreate(savedInstanceState)
lifecycleScope.launch {
@@ -213,28 +213,14 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
if (AppConfig.preDownloadNum == 0) return
cacheBookJob?.cancel()
cacheBookJob = viewModelScope.launch(upTocPool) {
while (isActive) {
if (CacheBookService.isRun || !CacheBook.isRun) {
cacheBookJob?.cancel()
cacheBookJob = null
return@launch
launch {
while (isActive && CacheBook.isRun) {
//有目录更新是不缓存,优先更新目录,现在更多网站限制并发
CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty())
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()
}
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 {
menuInflater.inflate(R.menu.replace_rule, menu)
return super.onCompatCreateOptionsMenu(menu)
@@ -203,6 +203,8 @@ class ReplaceEditActivity :
val edit = view.editableText
if (start < 0 || start >= edit.length) {
edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else {
edit.replace(start, end, text)
}
@@ -396,6 +396,8 @@ class RssSourceEditActivity :
val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else {
edit.replace(start, end, text)//光标所在位置插入文字
}
@@ -114,20 +114,6 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
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 {
menuInflater.inflate(R.menu.rss_source, menu)
return super.onCompatCreateOptionsMenu(menu)
@@ -5,6 +5,7 @@ import androidx.lifecycle.repeatOnLifecycle
import io.legado.app.data.appDb
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
@@ -43,7 +44,7 @@ inline fun <T> Flow<T>.onEachParallelSafe(
try {
action(value)
} catch (e: Throwable) {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
}
emit(value)
}
@@ -65,7 +66,7 @@ inline fun <T, R> Flow<T>.mapParallelSafe(
try {
emit(transform(value))
} catch (_: Throwable) {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
}
}
}.buffer(0)
@@ -79,7 +80,7 @@ inline fun <T, R> Flow<T>.transformParallelSafe(
try {
transform(value)
} catch (e: Throwable) {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
}
}
}.buffer(0)