mirror of
https://github.com/gedoor/legado.git
synced 2024-08-30 09:23:26 +08:00
优化
This commit is contained in:
parent
98e7721354
commit
c38b670b18
@ -170,6 +170,7 @@ dependencies {
|
||||
//lifecycle
|
||||
def lifecycle_version = '2.5.1'
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
|
||||
implementation("androidx.lifecycle:lifecycle-service:$lifecycle_version")
|
||||
|
||||
//compose
|
||||
// Integration with activities
|
||||
|
@ -1,15 +1,15 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import io.legado.app.help.LifecycleHelp
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseService : Service(), CoroutineScope by MainScope() {
|
||||
abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = this,
|
||||
@ -30,7 +30,8 @@ abstract class BaseService : Service(), CoroutineScope by MainScope() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -30,4 +30,5 @@ object EventBus {
|
||||
const val FILE_SOURCE_DOWNLOAD_DONE = "fileSourceDownloadDone"
|
||||
const val UPDATE_READ_ACTION_BAR = "updateReadActionBar"
|
||||
const val UP_SEEK_BAR = "upSeekBar"
|
||||
const val READ_ALOUD_PLAY = "readAloudPlay"
|
||||
}
|
@ -117,6 +117,8 @@ object PreferKey {
|
||||
const val progressBarBehavior = "progressBarBehavior"
|
||||
const val sourceEditMaxLine = "sourceEditMaxLine"
|
||||
const val ttsTimer = "ttsTimer"
|
||||
const val noAnimScrollPage = "noAnimScrollPage"
|
||||
const val webDavDeviceName = "webDavDeviceName"
|
||||
|
||||
const val cPrimary = "colorPrimary"
|
||||
const val cAccent = "colorAccent"
|
||||
|
@ -68,7 +68,12 @@ object AppWebDav {
|
||||
get() {
|
||||
val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
.format(Date(System.currentTimeMillis()))
|
||||
return "backup${backupDate}.zip"
|
||||
val deviceName = AppConfig.webDavDeviceName
|
||||
return if (deviceName?.isNotBlank() == true) {
|
||||
"backup${backupDate}-${deviceName}.zip"
|
||||
} else {
|
||||
"backup${backupDate}.zip"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upConfig() {
|
||||
|
@ -2,6 +2,7 @@ package io.legado.app.help.config
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.PreferKey
|
||||
@ -184,6 +185,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value)
|
||||
}
|
||||
|
||||
val noAnimScrollPage: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
|
||||
|
||||
const val defaultSpeechRate = 5
|
||||
|
||||
var ttsSpeechRate: Int
|
||||
@ -334,6 +338,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
val webDavDir get() = appCtx.getPrefString(PreferKey.webDavDir, "legado")
|
||||
|
||||
val webDavDeviceName get() = appCtx.getPrefString(PreferKey.webDavDeviceName, Build.MODEL)
|
||||
|
||||
val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog)
|
||||
|
||||
val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)
|
||||
|
@ -99,6 +99,7 @@ class Coroutine<T>(
|
||||
return this@Coroutine
|
||||
}
|
||||
|
||||
// 如果协程被取消,有可能会不执行
|
||||
fun onFinally(
|
||||
context: CoroutineContext? = null,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
|
@ -2,6 +2,8 @@ package io.legado.app.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.IntentAction
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
@ -10,6 +12,7 @@ import io.legado.app.service.BaseReadAloudService
|
||||
import io.legado.app.service.HttpReadAloudService
|
||||
import io.legado.app.service.TTSReadAloudService
|
||||
import io.legado.app.utils.StringUtils
|
||||
import io.legado.app.utils.postEvent
|
||||
import splitties.init.appCtx
|
||||
|
||||
object ReadAloud {
|
||||
@ -50,6 +53,19 @@ object ReadAloud {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
fun playByEventBus(
|
||||
play: Boolean = true,
|
||||
pageIndex: Int = ReadBook.durPageIndex,
|
||||
startPos: Int = 0
|
||||
) {
|
||||
val bundle = Bundle().apply {
|
||||
putBoolean("play", play)
|
||||
putInt("pageIndex", pageIndex)
|
||||
putInt("startPos", startPos)
|
||||
}
|
||||
postEvent(EventBus.READ_ALOUD_PLAY, bundle)
|
||||
}
|
||||
|
||||
fun pause(context: Context) {
|
||||
if (BaseReadAloudService.isRun) {
|
||||
val intent = Intent(context, aloudClass)
|
||||
|
@ -234,7 +234,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
private fun curPageChanged() {
|
||||
callBack?.pageChanged()
|
||||
if (BaseReadAloudService.isRun) {
|
||||
readAloud(!BaseReadAloudService.pause)
|
||||
ReadAloud.playByEventBus(!BaseReadAloudService.pause)
|
||||
}
|
||||
upReadTime()
|
||||
preDownload()
|
||||
|
@ -354,60 +354,63 @@ class AnalyzeUrl(
|
||||
return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait()))
|
||||
}
|
||||
val concurrentRecord = fetchStart()
|
||||
setCookie(source?.getKey())
|
||||
val strResponse: StrResponse
|
||||
if (this.useWebView && useWebView) {
|
||||
strResponse = when (method) {
|
||||
RequestMethod.POST -> {
|
||||
val res = getProxyClient(proxy).newCallStrResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
url(urlNoQuery)
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else {
|
||||
postJson(body)
|
||||
try {
|
||||
setCookie(source?.getKey())
|
||||
val strResponse: StrResponse
|
||||
if (this.useWebView && useWebView) {
|
||||
strResponse = when (method) {
|
||||
RequestMethod.POST -> {
|
||||
val res = getProxyClient(proxy).newCallStrResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
url(urlNoQuery)
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else {
|
||||
postJson(body)
|
||||
}
|
||||
}
|
||||
BackstageWebView(
|
||||
url = res.url,
|
||||
html = res.body,
|
||||
tag = source?.getKey(),
|
||||
javaScript = webJs ?: jsStr,
|
||||
sourceRegex = sourceRegex,
|
||||
headerMap = headerMap
|
||||
).getStrResponse()
|
||||
}
|
||||
BackstageWebView(
|
||||
url = res.url,
|
||||
html = res.body,
|
||||
else -> BackstageWebView(
|
||||
url = url,
|
||||
tag = source?.getKey(),
|
||||
javaScript = webJs ?: jsStr,
|
||||
sourceRegex = sourceRegex,
|
||||
headerMap = headerMap
|
||||
).getStrResponse()
|
||||
}
|
||||
else -> BackstageWebView(
|
||||
url = url,
|
||||
tag = source?.getKey(),
|
||||
javaScript = webJs ?: jsStr,
|
||||
sourceRegex = sourceRegex,
|
||||
headerMap = headerMap
|
||||
).getStrResponse()
|
||||
}
|
||||
} else {
|
||||
strResponse = getProxyClient(proxy).newCallStrResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
} else {
|
||||
strResponse = getProxyClient(proxy).newCallStrResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
}
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
}
|
||||
return strResponse
|
||||
} finally {
|
||||
fetchEnd(concurrentRecord)
|
||||
}
|
||||
fetchEnd(concurrentRecord)
|
||||
return strResponse
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@ -426,29 +429,32 @@ class AnalyzeUrl(
|
||||
*/
|
||||
suspend fun getResponseAwait(): Response {
|
||||
val concurrentRecord = fetchStart()
|
||||
setCookie(source?.getKey())
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val response = getProxyClient(proxy).newCallResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
try {
|
||||
setCookie(source?.getKey())
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val response = getProxyClient(proxy).newCallResponse(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
}
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
return response
|
||||
} finally {
|
||||
fetchEnd(concurrentRecord)
|
||||
}
|
||||
fetchEnd(concurrentRecord)
|
||||
return response
|
||||
}
|
||||
|
||||
fun getResponse(): Response {
|
||||
@ -460,40 +466,42 @@ class AnalyzeUrl(
|
||||
/**
|
||||
* 访问网站,返回ByteArray
|
||||
*/
|
||||
@Suppress("UnnecessaryVariable")
|
||||
suspend fun getByteArrayAwait(): ByteArray {
|
||||
val concurrentRecord = fetchStart()
|
||||
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
val dataUriFindResult = dataUriRegex.find(urlNoQuery)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
if (dataUriFindResult != null) {
|
||||
val dataUriBase64 = dataUriFindResult.groupValues[1]
|
||||
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
|
||||
fetchEnd(concurrentRecord)
|
||||
return byteArray
|
||||
} else {
|
||||
setCookie(source?.getKey())
|
||||
val byteArray = getProxyClient(proxy).newCallResponseBody(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
try {
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
val dataUriFindResult = dataUriRegex.find(urlNoQuery)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
if (dataUriFindResult != null) {
|
||||
val dataUriBase64 = dataUriFindResult.groupValues[1]
|
||||
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
|
||||
return byteArray
|
||||
} else {
|
||||
setCookie(source?.getKey())
|
||||
val byteArray = getProxyClient(proxy).newCallResponseBody(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
}
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
}.bytes()
|
||||
}.bytes()
|
||||
return byteArray
|
||||
}
|
||||
} finally {
|
||||
fetchEnd(concurrentRecord)
|
||||
return byteArray
|
||||
}
|
||||
}
|
||||
|
||||
@ -503,49 +511,9 @@ class AnalyzeUrl(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问网站,返回InputStream
|
||||
*/
|
||||
suspend fun getInputStreamAwait(): InputStream {
|
||||
val concurrentRecord = fetchStart()
|
||||
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
val dataUriFindResult = dataUriRegex.find(urlNoQuery)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
if (dataUriFindResult != null) {
|
||||
val dataUriBase64 = dataUriFindResult.groupValues[1]
|
||||
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
|
||||
fetchEnd(concurrentRecord)
|
||||
return ByteArrayInputStream(byteArray)
|
||||
} else {
|
||||
setCookie(source?.getKey())
|
||||
val inputStream = getProxyClient(proxy).newCallResponseBody(retry) {
|
||||
addHeaders(headerMap)
|
||||
when (method) {
|
||||
RequestMethod.POST -> {
|
||||
url(urlNoQuery)
|
||||
val contentType = headerMap["Content-Type"]
|
||||
val body = body
|
||||
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
|
||||
postForm(fieldMap, true)
|
||||
} else if (!contentType.isNullOrBlank()) {
|
||||
val requestBody = body.toRequestBody(contentType.toMediaType())
|
||||
post(requestBody)
|
||||
} else {
|
||||
postJson(body)
|
||||
}
|
||||
}
|
||||
else -> get(urlNoQuery, fieldMap, true)
|
||||
}
|
||||
}.byteStream()
|
||||
fetchEnd(concurrentRecord)
|
||||
return inputStream
|
||||
}
|
||||
}
|
||||
|
||||
fun getInputStream(): InputStream {
|
||||
return runBlocking {
|
||||
getInputStreamAwait()
|
||||
getResponseAwait().body!!.byteStream()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
@ -85,6 +86,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
wakeLock.acquire()
|
||||
isRun = true
|
||||
pause = false
|
||||
observeLiveBus()
|
||||
initMediaSession()
|
||||
initBroadcastReceiver()
|
||||
upNotification()
|
||||
@ -92,6 +94,15 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
setTimer(AppConfig.ttsTimer)
|
||||
}
|
||||
|
||||
fun observeLiveBus() {
|
||||
observeEvent<Bundle>(EventBus.READ_ALOUD_PLAY) {
|
||||
val play = it.getBoolean("play")
|
||||
val pageIndex = it.getInt("pageIndex")
|
||||
val startPos = it.getInt("startPos")
|
||||
newReadAloud(play, pageIndex, startPos)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
wakeLock.release()
|
||||
|
@ -11,6 +11,7 @@ import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.exception.ConcurrentException
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.config.AppConfig
|
||||
@ -20,9 +21,13 @@ import io.legado.app.model.ReadBook
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.Response
|
||||
import org.mozilla.javascript.WrappedException
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
@ -41,9 +46,9 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
||||
private var downloadTask: Coroutine<*>? = null
|
||||
private var playIndexJob: Job? = null
|
||||
private var downloadTaskIsActive = false
|
||||
private var downloadErrorNo: Int = 0
|
||||
private var playErrorNo = 0
|
||||
private val downloadTaskActiveLock = Mutex()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -71,7 +76,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
val file = getSpeakFileAsMd5(fileName)
|
||||
if (file.exists()) {
|
||||
playAudio(file)
|
||||
} else if (!downloadTaskIsActive) {
|
||||
} else if (!downloadTaskActiveLock.isLocked) {
|
||||
downloadAudio()
|
||||
}
|
||||
}
|
||||
@ -97,123 +102,111 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
}
|
||||
|
||||
private fun downloadAudio() {
|
||||
launch {
|
||||
downloadTask?.cancel()
|
||||
while (downloadTaskIsActive) {
|
||||
//在线tts大部分只能单线程,等待上次访问结束
|
||||
delay(100)
|
||||
}
|
||||
downloadTask = execute {
|
||||
downloadTask?.cancel()
|
||||
downloadTask = execute {
|
||||
downloadTaskActiveLock.withLock {
|
||||
ensureActive()
|
||||
removeCacheFile()
|
||||
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
|
||||
contentList.forEachIndexed { index, content ->
|
||||
ensureActive()
|
||||
val fileName = md5SpeakFileName(content)
|
||||
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
|
||||
if (hasSpeakFile(fileName)) { //已经下载好的语音缓存
|
||||
if (index == nowSpeak) {
|
||||
val file = getSpeakFileAsMd5(fileName)
|
||||
playAudio(file)
|
||||
}
|
||||
} else if (speakText.isEmpty()) {
|
||||
AppLog.put(
|
||||
"阅读段落内容为空,使用无声音频代替。\n朗读文本:$content"
|
||||
)
|
||||
if (speakText.isEmpty()) {
|
||||
AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$content")
|
||||
createSilentSound(fileName)
|
||||
if (index == nowSpeak) {
|
||||
val file = getSpeakFileAsMd5(fileName)
|
||||
playAudio(file)
|
||||
}
|
||||
return@forEachIndexed
|
||||
} else {
|
||||
} else if (!hasSpeakFile(fileName)) {
|
||||
runCatching {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
httpTts.url,
|
||||
speakText = speakText,
|
||||
speakSpeed = speechRate,
|
||||
source = httpTts,
|
||||
headerMapF = httpTts.getHeaderMap(true)
|
||||
)
|
||||
var response = analyzeUrl.getResponseAwait()
|
||||
ensureActive()
|
||||
httpTts.loginCheckJs?.takeIf { checkJs ->
|
||||
checkJs.isNotBlank()
|
||||
}?.let { checkJs ->
|
||||
response = analyzeUrl.evalJS(checkJs, response) as Response
|
||||
val inputStream = getSpeakStream(httpTts, speakText)
|
||||
if (inputStream != null) {
|
||||
createSpeakFile(fileName, inputStream)
|
||||
} else {
|
||||
createSilentSound(fileName)
|
||||
}
|
||||
httpTts.contentType?.takeIf { ct ->
|
||||
ct.isNotBlank()
|
||||
}?.let { ct ->
|
||||
response.headers["Content-Type"]?.let { contentType ->
|
||||
if (!contentType.matches(ct.toRegex())) {
|
||||
throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string())
|
||||
}
|
||||
}
|
||||
}
|
||||
ensureActive()
|
||||
response.body!!.bytes().let { bytes ->
|
||||
val file = createSpeakFileAsMd5IfNotExist(fileName)
|
||||
file.writeBytes(bytes)
|
||||
if (index == nowSpeak) {
|
||||
playAudio(file)
|
||||
}
|
||||
}
|
||||
downloadErrorNo = 0
|
||||
}.onFailure {
|
||||
when (it) {
|
||||
is CancellationException -> Unit
|
||||
is ConcurrentException -> {
|
||||
delay(it.waitTime.toLong())
|
||||
downloadAudio()
|
||||
}
|
||||
is ScriptException, is WrappedException -> {
|
||||
AppLog.put("js错误\n${it.localizedMessage}", it)
|
||||
toastOnUi("js错误\n${it.localizedMessage}")
|
||||
it.printOnDebug()
|
||||
cancel()
|
||||
pauseReadAloud()
|
||||
}
|
||||
is SocketTimeoutException, is ConnectException -> {
|
||||
downloadErrorNo++
|
||||
if (downloadErrorNo > 5) {
|
||||
val msg = "tts超时或连接错误超过5次\n${it.localizedMessage}"
|
||||
AppLog.put(msg, it)
|
||||
toastOnUi(msg)
|
||||
pauseReadAloud()
|
||||
} else {
|
||||
downloadAudio()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
downloadErrorNo++
|
||||
val msg = "tts下载错误\n${it.localizedMessage}"
|
||||
AppLog.put(msg, it)
|
||||
it.printOnDebug()
|
||||
if (downloadErrorNo > 5) {
|
||||
AppLog.put("TTS服务器连续5次错误,已暂停阅读。")
|
||||
toastOnUi("TTS服务器连续5次错误,已暂停阅读。")
|
||||
pauseReadAloud()
|
||||
} else {
|
||||
AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$content")
|
||||
createSilentSound(fileName)
|
||||
if (index == nowSpeak) {
|
||||
val file = getSpeakFileAsMd5(fileName)
|
||||
playAudio(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> pauseReadAloud()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
}
|
||||
if (index == nowSpeak) {
|
||||
val file = getSpeakFileAsMd5(fileName)
|
||||
playAudio(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onError(IO) {
|
||||
AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSpeakStream(httpTts: HttpTTS, speakText: String): InputStream? {
|
||||
while (true) {
|
||||
try {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
httpTts.url,
|
||||
speakText = speakText,
|
||||
speakSpeed = speechRate,
|
||||
source = httpTts,
|
||||
headerMapF = httpTts.getHeaderMap(true)
|
||||
)
|
||||
var response = analyzeUrl.getResponseAwait()
|
||||
ensureActive()
|
||||
val checkJs = httpTts.loginCheckJs
|
||||
if (checkJs?.isNotBlank() == true) {
|
||||
response = analyzeUrl.evalJS(checkJs, response) as Response
|
||||
}
|
||||
val ct = httpTts.contentType
|
||||
if (ct?.isNotBlank() == true) {
|
||||
response.headers["Content-Type"]?.let { contentType ->
|
||||
if (!contentType.matches(ct.toRegex())) {
|
||||
throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string())
|
||||
}
|
||||
}
|
||||
}
|
||||
ensureActive()
|
||||
downloadErrorNo = 0
|
||||
return response.body!!.byteStream()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is CancellationException -> throw e
|
||||
is ConcurrentException -> delay(e.waitTime.toLong())
|
||||
is ScriptException, is WrappedException -> {
|
||||
AppLog.put("js错误\n${e.localizedMessage}", e)
|
||||
toastOnUi("js错误\n${e.localizedMessage}")
|
||||
e.printOnDebug()
|
||||
throw e
|
||||
}
|
||||
is SocketTimeoutException, is ConnectException -> {
|
||||
downloadErrorNo++
|
||||
if (downloadErrorNo > 5) {
|
||||
val msg = "tts超时或连接错误超过5次\n${e.localizedMessage}"
|
||||
AppLog.put(msg, e)
|
||||
toastOnUi(msg)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
downloadErrorNo++
|
||||
val msg = "tts下载错误\n${e.localizedMessage}"
|
||||
AppLog.put(msg, e)
|
||||
e.printOnDebug()
|
||||
if (downloadErrorNo > 5) {
|
||||
val msg1 = "TTS服务器连续5次错误,已暂停阅读。"
|
||||
AppLog.put(msg1)
|
||||
toastOnUi(msg1)
|
||||
throw e
|
||||
} else {
|
||||
AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$speakText")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
downloadTaskIsActive = true
|
||||
}.onError {
|
||||
AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
|
||||
}.onFinally {
|
||||
downloadTaskIsActive = false
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -238,7 +231,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
}
|
||||
|
||||
private fun createSilentSound(fileName: String) {
|
||||
val file = createSpeakFileAsMd5IfNotExist(fileName)
|
||||
val file = createSpeakFile(fileName)
|
||||
file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes())
|
||||
}
|
||||
|
||||
@ -250,10 +243,18 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
return File("${ttsFolderPath}$name.mp3")
|
||||
}
|
||||
|
||||
private fun createSpeakFileAsMd5IfNotExist(name: String): File {
|
||||
private fun createSpeakFile(name: String): File {
|
||||
return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3")
|
||||
}
|
||||
|
||||
private fun createSpeakFile(name: String, inputStream: InputStream) {
|
||||
FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3").outputStream().use { out ->
|
||||
inputStream.use {
|
||||
it.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除缓存文件
|
||||
*/
|
||||
|
@ -140,6 +140,9 @@ class MoreConfigDialog : DialogFragment() {
|
||||
PreferKey.progressBarBehavior -> {
|
||||
postEvent(EventBus.UP_SEEK_BAR, true)
|
||||
}
|
||||
PreferKey.noAnimScrollPage -> {
|
||||
ReadBook.callBack?.upPageAnim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,6 @@ class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) {
|
||||
private fun initData() = binding.run {
|
||||
upPlayState()
|
||||
upTimerText(BaseReadAloudService.timeMinute)
|
||||
seekTimer.progress = BaseReadAloudService.timeMinute
|
||||
cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
|
||||
upTtsSpeechRateEnabled(!cbTtsFollowSys.isChecked)
|
||||
upSeekTimer()
|
||||
@ -170,7 +169,7 @@ class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) {
|
||||
if (BaseReadAloudService.timeMinute > 0) {
|
||||
binding.seekTimer.progress = BaseReadAloudService.timeMinute
|
||||
} else {
|
||||
binding.seekTimer.progress = 0
|
||||
binding.seekTimer.progress = AppConfig.ttsTimer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -503,6 +503,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
pageDelegate = NoAnimPageDelegate(this)
|
||||
}
|
||||
}
|
||||
(pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,8 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
|
||||
//速度追踪器
|
||||
private val mVelocity: VelocityTracker = VelocityTracker.obtain()
|
||||
|
||||
var noAnim: Boolean = false
|
||||
|
||||
override fun onAnimStart(animationSpeed: Int) {
|
||||
//惯性滚动
|
||||
fling(
|
||||
@ -104,6 +106,10 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
|
||||
if (readView.isAbortAnim) {
|
||||
return
|
||||
}
|
||||
if (noAnim) {
|
||||
curPage.scroll(calcNextPageOffset())
|
||||
return
|
||||
}
|
||||
readView.setStartPoint(0f, 0f, false)
|
||||
startScroll(0, 0, 0, calcNextPageOffset(), animationSpeed)
|
||||
}
|
||||
@ -112,6 +118,10 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
|
||||
if (readView.isAbortAnim) {
|
||||
return
|
||||
}
|
||||
if (noAnim) {
|
||||
curPage.scroll(calcPrevPageOffset())
|
||||
return
|
||||
}
|
||||
readView.setStartPoint(0f, 0f, false)
|
||||
startScroll(0, 0, 0, calcPrevPageOffset(), animationSpeed)
|
||||
}
|
||||
|
@ -122,14 +122,17 @@ class BackupConfigFragment : PreferenceFragment(),
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.text = AppConfig.webDavDir?.toEditable()
|
||||
}
|
||||
it.setOnPreferenceChangeListener { _, newValue ->
|
||||
(newValue as String).isNotBlank()
|
||||
}
|
||||
findPreference<EditTextPreference>(PreferKey.webDavDeviceName)?.let {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.text = AppConfig.webDavDeviceName?.toEditable()
|
||||
}
|
||||
}
|
||||
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
|
||||
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
|
||||
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
|
||||
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
|
||||
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
|
||||
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
|
||||
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
|
||||
?.onLongClick { restoreDir.launch(); true }
|
||||
|
@ -4,6 +4,7 @@ package io.legado.app.utils
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.Observer
|
||||
import com.jeremyliao.liveeventbus.LiveEventBus
|
||||
import com.jeremyliao.liveeventbus.core.Observable
|
||||
@ -70,4 +71,28 @@ inline fun <reified EVENT> Fragment.observeEventSticky(
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observeSticky(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> LifecycleService.observeEvent(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observe(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> LifecycleService.observeEventSticky(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observeSticky(this, o)
|
||||
}
|
||||
}
|
@ -1043,4 +1043,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1046,4 +1046,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1046,4 +1046,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1043,4 +1043,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1045,4 +1045,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1045,4 +1045,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -1046,4 +1046,6 @@
|
||||
<string name="search_scope">搜索范围</string>
|
||||
<string name="toggle_search_scope">切换</string>
|
||||
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
|
||||
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
|
||||
<string name="webdav_device_name">设备名称</string>
|
||||
</resources>
|
||||
|
@ -34,6 +34,13 @@
|
||||
app:allowDividerBelow="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<io.legado.app.lib.prefs.EditTextPreference
|
||||
android:key="webDavDeviceName"
|
||||
android:title="@string/webdav_device_name"
|
||||
app:allowDividerAbove="false"
|
||||
app:allowDividerBelow="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<io.legado.app.lib.prefs.SwitchPreference
|
||||
android:key="syncBookProgress"
|
||||
android:defaultValue="true"
|
||||
|
@ -136,6 +136,13 @@
|
||||
app:iconSpaceReserved="false"
|
||||
app:isBottomBackground="true" />
|
||||
|
||||
<io.legado.app.lib.prefs.SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="noAnimScrollPage"
|
||||
android:title="@string/no_anim_scroll_page"
|
||||
app:iconSpaceReserved="false"
|
||||
app:isBottomBackground="true" />
|
||||
|
||||
<io.legado.app.lib.prefs.Preference
|
||||
android:key="clickRegionalConfig"
|
||||
android:title="@string/click_regional_config"
|
||||
|
Loading…
Reference in New Issue
Block a user