diff --git a/app/build.gradle b/app/build.gradle index 465f241f8..6863facfd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt index 852dc0d5f..b64aaeb4d 100644 --- a/app/src/main/java/io/legado/app/base/BaseService.kt +++ b/app/src/main/java/io/legado/app/base/BaseService.kt @@ -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 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 } diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt index ba813746a..1d9b3e9e1 100644 --- a/app/src/main/java/io/legado/app/constant/EventBus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 84a190b07..34f19c7da 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -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" diff --git a/app/src/main/java/io/legado/app/help/AppWebDav.kt b/app/src/main/java/io/legado/app/help/AppWebDav.kt index 7af00bd18..c797911a5 100644 --- a/app/src/main/java/io/legado/app/help/AppWebDav.kt +++ b/app/src/main/java/io/legado/app/help/AppWebDav.kt @@ -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() { diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 46f3f9134..a792b9754 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt index 72e87ed5f..4c83154a3 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt @@ -99,6 +99,7 @@ class Coroutine( return this@Coroutine } + // 如果协程被取消,有可能会不执行 fun onFinally( context: CoroutineContext? = null, block: suspend CoroutineScope.() -> Unit diff --git a/app/src/main/java/io/legado/app/model/ReadAloud.kt b/app/src/main/java/io/legado/app/model/ReadAloud.kt index 2c94b7d56..de7b887e0 100644 --- a/app/src/main/java/io/legado/app/model/ReadAloud.kt +++ b/app/src/main/java/io/legado/app/model/ReadAloud.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/model/ReadBook.kt b/app/src/main/java/io/legado/app/model/ReadBook.kt index 6fed0d013..6060a6af6 100644 --- a/app/src/main/java/io/legado/app/model/ReadBook.kt +++ b/app/src/main/java/io/legado/app/model/ReadBook.kt @@ -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() diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index a2d71f570..ff2ebb698 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -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() } } diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index f0bb0d54c..9b2c9553e 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -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(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() diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index f6f022513..77d03476d 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -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) + } + } + } + /** * 移除缓存文件 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt index 170068d21..a4ed8c938 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt @@ -140,6 +140,9 @@ class MoreConfigDialog : DialogFragment() { PreferKey.progressBarBehavior -> { postEvent(EventBus.UP_SEEK_BAR, true) } + PreferKey.noAnimScrollPage -> { + ReadBook.callBack?.upPageAnim() + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt index c799f9ddf..691e3d72b 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt @@ -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 } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt index 80ce5c30f..47f56999e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt @@ -503,6 +503,7 @@ class ReadView(context: Context, attrs: AttributeSet) : pageDelegate = NoAnimPageDelegate(this) } } + (pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage } /** diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt index fefeba8d6..1a8bd41a2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -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) } diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt index 8c22a534d..c390af8c6 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -122,14 +122,17 @@ class BackupConfigFragment : PreferenceFragment(), it.setOnBindEditTextListener { editText -> editText.text = AppConfig.webDavDir?.toEditable() } - it.setOnPreferenceChangeListener { _, newValue -> - (newValue as String).isNotBlank() + } + findPreference(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("web_dav_restore") ?.onLongClick { restoreDir.launch(); true } diff --git a/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt index 0293e4893..13947c893 100644 --- a/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt @@ -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 Fragment.observeEventSticky( tags.forEach { eventObservable(it).observeSticky(this, o) } +} + +inline fun LifecycleService.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + +inline fun LifecycleService.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } } \ No newline at end of file diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index aad0c6af2..8babdce11 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -1043,4 +1043,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index f62aad3c9..d1682a054 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -1046,4 +1046,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 850e14510..bcd59fc9d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1046,4 +1046,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 396b0d9c0..2a2ba3e85 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1043,4 +1043,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 23d613c6d..11331cbe4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1045,4 +1045,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index ce56df140..cb7834cdb 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1045,4 +1045,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df892a697..f0abcea4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1046,4 +1046,6 @@ 搜索范围 切换 是否确认清除所有搜索历史记录 + 禁用滚动点击动画 + 设备名称 diff --git a/app/src/main/res/xml/pref_config_backup.xml b/app/src/main/res/xml/pref_config_backup.xml index faf7e214b..f35e73fb6 100644 --- a/app/src/main/res/xml/pref_config_backup.xml +++ b/app/src/main/res/xml/pref_config_backup.xml @@ -34,6 +34,13 @@ app:allowDividerBelow="false" app:iconSpaceReserved="false" /> + + + +