This commit is contained in:
Horis 2022-10-19 17:53:24 +08:00
parent 98e7721354
commit c38b670b18
27 changed files with 324 additions and 242 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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"
}

View File

@ -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"

View File

@ -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() {

View File

@ -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)

View File

@ -99,6 +99,7 @@ class Coroutine<T>(
return this@Coroutine
}
// 如果协程被取消,有可能会不执行
fun onFinally(
context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit

View File

@ -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)

View File

@ -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()

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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)
}
}
}
/**
* 移除缓存文件
*/

View File

@ -140,6 +140,9 @@ class MoreConfigDialog : DialogFragment() {
PreferKey.progressBarBehavior -> {
postEvent(EventBus.UP_SEEK_BAR, true)
}
PreferKey.noAnimScrollPage -> {
ReadBook.callBack?.upPageAnim()
}
}
}

View File

@ -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
}
}
}

View File

@ -503,6 +503,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
pageDelegate = NoAnimPageDelegate(this)
}
}
(pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage
}
/**

View File

@ -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)
}

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"