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