Merge remote-tracking branch 'origin/master'

This commit is contained in:
kunfei 2023-06-21 17:00:37 +08:00
commit ab54311d51
18 changed files with 100 additions and 67 deletions

View File

@ -18,8 +18,9 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
start: CoroutineStart = CoroutineStart.DEFAULT,
executeContext: CoroutineContext = Dispatchers.Main,
block: suspend CoroutineScope.() -> T
) = Coroutine.async(scope, context, start) { block() }
) = Coroutine.async(scope, context, start, executeContext, block)
@CallSuper
override fun onCreate() {
@ -57,7 +58,7 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
/**
* 检测通知权限
*/
private fun checkNotificationPermission() {
private fun checkNotificationPermission() {
PermissionsCompat.Builder()
.addPermissions(Permissions.POST_NOTIFICATIONS)
.rationale(R.string.notification_permission_rationale)

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.App
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
@ -19,9 +20,11 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio
fun <T> execute(
scope: CoroutineScope = viewModelScope,
context: CoroutineContext = Dispatchers.IO,
start: CoroutineStart = CoroutineStart.DEFAULT,
executeContext: CoroutineContext = Dispatchers.Main,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
return Coroutine.async(scope, context, start, executeContext, block)
}
fun <R> submit(

View File

@ -183,7 +183,7 @@ data class BookSource(
fun getInvalidGroupNames(): String {
return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter {
"失效" in it
"失效" in it || it == "校验超时"
}?.joinToString() ?: ""
}

View File

@ -25,6 +25,7 @@ class Coroutine<T>(
val scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO,
val startOption: CoroutineStart = CoroutineStart.DEFAULT,
val executeContext: CoroutineContext = Dispatchers.Main,
block: suspend CoroutineScope.() -> T
) {
@ -36,9 +37,10 @@ class Coroutine<T>(
scope: CoroutineScope = DEFAULT,
context: CoroutineContext = Dispatchers.IO,
start: CoroutineStart = CoroutineStart.DEFAULT,
executeContext: CoroutineContext = Dispatchers.Main,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine(scope, context, start, block)
return Coroutine(scope, context, start, executeContext, block)
}
}
@ -158,7 +160,7 @@ class Coroutine<T>(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): Job {
return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
return (scope.plus(executeContext)).launch(start = startOption) {
try {
start?.let { dispatchVoidCallback(this, it) }
ensureActive()

View File

@ -57,9 +57,8 @@ object LocalBook {
}.firstOrNull()?.let {
getBookInputStream(it)
}
} else if (webDavUrl != null) {
} else if (webDavUrl != null && downloadRemoteBook(book)) {
// 下载远程链接
downloadRemoteBook(book)
getBookInputStream(book)
} else {
null
@ -90,12 +89,15 @@ object LocalBook {
book.isEpub -> {
EpubFile.getChapterList(book)
}
book.isUmd -> {
UmdFile.getChapterList(book)
}
book.isPdf -> {
PdfFile.getChapterList(book)
}
else -> {
TextFile.getChapterList(book)
}
@ -117,12 +119,15 @@ object LocalBook {
book.isEpub -> {
EpubFile.getContent(book, chapter)
}
book.isUmd -> {
UmdFile.getContent(book, chapter)
}
book.isPdf -> {
PdfFile.getContent(book, chapter)
}
else -> {
TextFile.getContent(book, chapter)
}
@ -218,7 +223,7 @@ object LocalBook {
}
}
/* 批量导入 支持自动导入压缩包的支持书籍 */
/* 批量导入 支持自动导入压缩包的支持书籍 */
fun importFiles(uri: Uri): List<Book> {
val books = mutableListOf<Book>()
val fileDoc = FileDoc.fromUri(uri, false)
@ -329,6 +334,7 @@ object LocalBook {
Base64.DEFAULT
)
)
else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
}
return saveBookFile(inputStream, fileName)
@ -383,7 +389,7 @@ object LocalBook {
}
//下载book对应的远程文件 并更新Book
private fun downloadRemoteBook(localBook: Book) {
private fun downloadRemoteBook(localBook: Book): Boolean {
val webDavUrl = localBook.getRemoteUrl()
if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
try {
@ -415,9 +421,11 @@ object LocalBook {
localBook.save()
}
}
return true
} catch (e: Exception) {
e.printOnDebug()
AppLog.put("自动下载webDav书籍失败", e)
return false
}
}

View File

@ -122,7 +122,11 @@ class CheckSourceService : BaseService() {
*校验书源
*/
private fun check(source: BookSource) {
execute(context = searchCoroutine, start = CoroutineStart.LAZY) {
execute(
context = searchCoroutine,
start = CoroutineStart.LAZY,
executeContext = IO
) {
Debug.startChecking(source)
var searchWord = CheckSource.keyword
source.ruleSearch?.checkKeyWord?.let {
@ -188,7 +192,6 @@ class CheckSourceService : BaseService() {
"" else "\n\n${source.bookSourceComment}"
Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
}.onSuccess(searchCoroutine) {
source.removeGroup("校验超时")
Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
}.onFinally(IO) {
source.respondTime = Debug.getRespondTime(source.bookSourceUrl)

View File

@ -26,7 +26,7 @@ import io.legado.app.utils.printOnDebug
import io.legado.app.utils.servicePendingIntent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
@ -68,32 +68,21 @@ class HttpReadAloudService : BaseReadAloudService(),
super.onDestroy()
downloadTask?.cancel()
exoPlayer.release()
Coroutine.async {
removeCacheFile()
}
}
override fun play() {
pageChanged = false
exoPlayer.stop()
if (!requestFocus()) return
if (contentList.isEmpty()) {
AppLog.putDebug("朗读列表为空")
ReadBook.readAloud()
} else {
super.play()
kotlin.runCatching {
if (nowSpeak == 0) {
downloadAudio()
} else {
val fileName = md5SpeakFileName(contentList[nowSpeak])
val file = getSpeakFileAsMd5(fileName)
if (file.exists()) {
playAudio(file)
} else if (!downloadTaskActiveLock.isLocked) {
downloadAudio()
}
}
}.onFailure {
toastOnUi("朗读出错:${it.localizedMessage}")
AppLog.put("朗读出错:${it.localizedMessage}", it)
}
downloadAndPlayAudios()
}
}
@ -105,21 +94,21 @@ class HttpReadAloudService : BaseReadAloudService(),
readAloudNumber += contentList[nowSpeak].length + 1
if (nowSpeak < contentList.lastIndex) {
nowSpeak++
play()
} else {
nextChapter()
}
}
private fun downloadAudio() {
private fun downloadAndPlayAudios() {
exoPlayer.clearMediaItems()
downloadTask?.cancel()
downloadTask = execute {
downloadTaskActiveLock.withLock {
ensureActive()
removeCacheFile()
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
contentList.forEachIndexed { index, content ->
ensureActive()
if (index < nowSpeak) return@forEachIndexed
val fileName = md5SpeakFileName(content)
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
if (speakText.isEmpty()) {
@ -141,13 +130,23 @@ class HttpReadAloudService : BaseReadAloudService(),
return@execute
}
}
if (index == nowSpeak) {
val file = getSpeakFileAsMd5(fileName)
playAudio(file)
val file = getSpeakFileAsMd5(fileName)
val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
launch(Main) {
if (exoPlayer.playbackState == Player.STATE_ENDED) {
exoPlayer.stop()
exoPlayer.clearMediaItems()
}
exoPlayer.addMediaItem(mediaItem)
if (!exoPlayer.isPlaying) {
exoPlayer.playWhenReady = !pause
exoPlayer.prepare()
}
}
}
}
}.onError(IO) {
}.onError {
toastOnUi("朗读出错:${it.localizedMessage}")
AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
}
}
@ -193,6 +192,7 @@ class HttpReadAloudService : BaseReadAloudService(),
e.printOnDebug()
throw e
}
is SocketTimeoutException, is ConnectException -> {
downloadErrorNo++
if (downloadErrorNo > 5) {
@ -202,6 +202,7 @@ class HttpReadAloudService : BaseReadAloudService(),
throw e
}
}
else -> {
downloadErrorNo++
val msg = "tts下载错误\n${e.localizedMessage}"
@ -223,22 +224,6 @@ class HttpReadAloudService : BaseReadAloudService(),
return null
}
@Synchronized
private fun playAudio(file: File) {
if (requestFocus()) {
launch {
kotlin.runCatching {
val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
exoPlayer.setMediaItem(mediaItem)
exoPlayer.playWhenReady = true
exoPlayer.prepare()
}.onFailure {
it.printOnDebug()
}
}
}
}
private fun md5SpeakFileName(content: String): String {
return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
@ -338,7 +323,7 @@ class HttpReadAloudService : BaseReadAloudService(),
downloadTask?.cancel()
exoPlayer.stop()
speechRate = AppConfig.speechRatePlay + 5
downloadAudio()
downloadAndPlayAudios()
}
override fun onPlaybackStateChanged(playbackState: Int) {
@ -347,15 +332,18 @@ class HttpReadAloudService : BaseReadAloudService(),
Player.STATE_IDLE -> {
// 空闲
}
Player.STATE_BUFFERING -> {
// 缓冲中
}
Player.STATE_READY -> {
// 准备好
if (pause) return
exoPlayer.play()
upPlayPos()
}
Player.STATE_ENDED -> {
// 结束
playErrorNo = 0
@ -364,6 +352,12 @@ class HttpReadAloudService : BaseReadAloudService(),
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return
upPlayPos()
playNext()
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
AppLog.put("朗读错误\n${contentList[nowSpeak]}", error)

View File

@ -44,6 +44,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/**
@ -204,7 +205,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
}
}
launch {
appDb.bookSourceDao.flowEnabledGroups().conflate().collect {
appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect {
groups.clear()
groups.addAll(it)
upGroupMenu()

View File

@ -47,8 +47,10 @@ import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
@ -230,7 +232,7 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
}
}
launch {
appDb.bookSourceDao.flowEnabledGroups().conflate().collect {
appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect {
groups.clear()
groups.addAll(it)
upGroupMenu()

View File

@ -108,7 +108,8 @@ abstract class BaseReadBookActivity :
*/
fun upSystemUiVisibility(
isInMultiWindow: Boolean,
toolBarHide: Boolean = true
toolBarHide: Boolean = true,
useBgMeanColor: Boolean = false
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.run {
@ -129,7 +130,10 @@ abstract class BaseReadBookActivity :
setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark())
} else {
val statusBarColor =
if (AppConfig.readBarStyleFollowPage && ReadBookConfig.durConfig.curBgType() == 0) {
if (AppConfig.readBarStyleFollowPage
&& ReadBookConfig.durConfig.curBgType() == 0
|| useBgMeanColor
) {
ReadBookConfig.bgMeanColor
} else {
ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar)
@ -184,12 +188,14 @@ abstract class BaseReadBookActivity :
width = MATCH_PARENT
gravity = Gravity.BOTTOM
}
Gravity.LEFT -> layoutParams =
(layoutParams as FrameLayout.LayoutParams).apply {
height = MATCH_PARENT
width = navigationBarHeight
gravity = Gravity.LEFT
}
Gravity.RIGHT -> layoutParams =
(layoutParams as FrameLayout.LayoutParams).apply {
height = MATCH_PARENT

View File

@ -1044,7 +1044,7 @@ class ReadBookActivity : BaseReadBookActivity(),
* 更新状态栏,导航栏
*/
override fun upSystemUiVisibility() {
upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible)
upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible, bottomDialog > 0)
upNavigationBarColor()
}

View File

@ -93,7 +93,7 @@ class SpeakEngineDialog(val callBack: CallBack) : BaseDialogFragment(R.layout.di
labelSys.visible()
cbName.text = "系统默认"
cbName.tag = ""
cbName.isChecked = ttsEngine == null || ttsEngine!!.toIntOrNull() == null
cbName.isChecked = ttsEngine == null || ttsEngine!!.isJsonObject()
&& GSON.fromJsonObject<SelectItem<String>>(ttsEngine)
.getOrNull()?.value.isNullOrEmpty()
cbName.setOnClickListener {

View File

@ -31,6 +31,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.init.appCtx
@ -279,7 +280,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
adapter.setItems(it)
}
launch {
appDb.bookSourceDao.flowEnabledGroups().collect {
appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).collect {
groups = it
}
}

View File

@ -42,9 +42,11 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
/**
@ -321,7 +323,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
}
}.catch {
AppLog.put("书源界面更新书源出错", it)
}.conflate().collect { data ->
}.flowOn(IO).conflate().collect { data ->
adapter.setItems(data, adapter.diffItemCallback)
itemTouchCallback.isCanDrag = sort == BookSourceSort.Default
delay(500)
@ -336,10 +338,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun initLiveDataGroup() {
launch {
appDb.bookSourceDao.flowGroups().conflate().collect {
appDb.bookSourceDao.flowGroups().flowOn(IO).conflate().collect {
groups.clear()
groups.addAll(it)
upGroupMenu()
delay(500)
}
}
}

View File

@ -25,6 +25,8 @@ import io.legado.app.utils.applyTint
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
@ -55,7 +57,7 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
private fun initData() {
launch {
appDb.bookSourceDao.flowGroups().collect {
appDb.bookSourceDao.flowGroups().flowOn(IO).collect {
adapter.setItems(it)
}
}

View File

@ -20,6 +20,7 @@ import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService
import io.legado.app.utils.postEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
import kotlin.math.min
@ -85,9 +86,11 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
upTocJob?.cancel()
upTocJob = null
}
onUpTocBooks.size < threadCount -> {
updateToc()
}
else -> {
delay(500)
}
@ -119,7 +122,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
}
waitUpTocBooks.remove(bookUrl)
upTocAdd(bookUrl)
execute(context = upTocPool) {
execute(context = upTocPool, executeContext = IO) {
kotlin.runCatching {
val oldBook = book.copy()
WebBook.runPreUpdateJs(source, book)

View File

@ -29,9 +29,11 @@ import io.legado.app.utils.setEdgeEffectColor
import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/**
@ -107,7 +109,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
private fun initGroupData() {
launch {
appDb.bookSourceDao.flowExploreGroups().conflate().collect {
appDb.bookSourceDao.flowExploreGroups().flowOn(IO).conflate().collect {
groups.clear()
groups.addAll(it)
upGroupsMenu()

View File

@ -11,6 +11,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:singleLine="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
@ -25,12 +26,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:singleLine="true"
android:textColor="@color/secondaryText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/cb_source_name"
app:layout_constraintRight_toLeftOf="@id/tv_open"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="packed" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_open"