Merge branch 'gedoor:master' into master
This commit is contained in:
@@ -58,7 +58,7 @@ class App : MultiDexApplication() {
|
||||
2 -> ChineseUtils.s2t("初始化")
|
||||
}
|
||||
//同步阅读记录
|
||||
if (AppWebDav.isOk && AppConfig.syncBookProgress) {
|
||||
if (AppWebDav.syncBookProgress) {
|
||||
AppWebDav.downloadAllBookProgress()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ object EventBus {
|
||||
const val BOOKSHELF_REFRESH = "bookshelfRefresh"
|
||||
const val ALOUD_STATE = "aloud_state"
|
||||
const val TTS_PROGRESS = "ttsStart"
|
||||
const val TTS_DS = "ttsDs"
|
||||
const val AUDIO_DS = "audioDs"
|
||||
const val READ_ALOUD_DS = "readAloudDs"
|
||||
const val BATTERY_CHANGED = "batteryChanged"
|
||||
const val TIME_CHANGED = "timeChanged"
|
||||
const val UP_CONFIG = "upConfig"
|
||||
@@ -17,13 +18,11 @@ object EventBus {
|
||||
const val AUDIO_PROGRESS = "audioProgress"
|
||||
const val AUDIO_SIZE = "audioSize"
|
||||
const val AUDIO_SPEED = "audioSpeed"
|
||||
const val AUDIO_ERROR = "audioError"
|
||||
const val NOTIFY_MAIN = "notifyMain"
|
||||
const val WEB_SERVICE = "webService"
|
||||
const val UP_DOWNLOAD = "upDownload"
|
||||
const val SAVE_CONTENT = "saveContent"
|
||||
const val CHECK_SOURCE = "checkSource"
|
||||
const val CHECK_SOURCE_MESSAGE = "checkSourceMessage"
|
||||
const val CHECK_SOURCE_DONE = "checkSourceDone"
|
||||
const val TIP_COLOR = "tipColor"
|
||||
const val SOURCE_CHANGED = "sourceChanged"
|
||||
|
||||
@@ -282,8 +282,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
val importKeepName get() = appCtx.getPrefBoolean(PreferKey.importKeepName)
|
||||
|
||||
val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true)
|
||||
|
||||
var preDownloadNum
|
||||
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
||||
set(value) {
|
||||
|
||||
@@ -29,6 +29,8 @@ object AppWebDav {
|
||||
private val bookProgressUrl = "${rootWebDavUrl}bookProgress/"
|
||||
private val zipFilePath = "${appCtx.externalFiles.absolutePath}${File.separator}backup.zip"
|
||||
|
||||
val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true)
|
||||
|
||||
var isOk = false
|
||||
|
||||
init {
|
||||
@@ -167,7 +169,7 @@ object AppWebDav {
|
||||
|
||||
fun uploadBookProgress(book: Book) {
|
||||
if (!isOk) return
|
||||
if (!AppConfig.syncBookProgress) return
|
||||
if (!syncBookProgress) return
|
||||
if (!NetworkUtils.isAvailable()) return
|
||||
Coroutine.async {
|
||||
val bookProgress = BookProgress(book)
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.service.AudioPlayService
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.startService
|
||||
import splitties.init.appCtx
|
||||
|
||||
object AudioPlay {
|
||||
var titleData = MutableLiveData<String>()
|
||||
@@ -138,10 +139,17 @@ object AudioPlay {
|
||||
}
|
||||
}
|
||||
|
||||
fun addTimer(context: Context) {
|
||||
val intent = Intent(context, AudioPlayService::class.java)
|
||||
fun addTimer() {
|
||||
val intent = Intent(appCtx, AudioPlayService::class.java)
|
||||
intent.action = IntentAction.addTimer
|
||||
context.startService(intent)
|
||||
appCtx.startService(intent)
|
||||
}
|
||||
|
||||
fun setTimer(minute: Int) {
|
||||
val intent = Intent(appCtx, AudioPlayService::class.java)
|
||||
intent.action = IntentAction.setTimer
|
||||
intent.putExtra("minute", minute)
|
||||
appCtx.startService(intent)
|
||||
}
|
||||
|
||||
fun saveRead(book: Book) {
|
||||
|
||||
@@ -16,12 +16,17 @@ import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import javax.script.SimpleBindings
|
||||
|
||||
object LocalBook {
|
||||
|
||||
private val nameAuthorPatterns = arrayOf(
|
||||
Pattern.compile("(.*?)《([^《》]+)》(.*)"),
|
||||
Pattern.compile("(^)(.+) 作者:(.+)$"),
|
||||
Pattern.compile("(^)(.+) by (.+)$")
|
||||
)
|
||||
|
||||
@Throws(FileNotFoundException::class, SecurityException::class)
|
||||
fun getBookInputStream(book: Book): InputStream {
|
||||
val uri = Uri.parse(book.bookUrl)
|
||||
@@ -117,44 +122,37 @@ object LocalBook {
|
||||
val tempFileName = fileName.substringBeforeLast(".")
|
||||
var name: String
|
||||
var author: String
|
||||
//匹配(知轩藏书常用格式) 《书名》其它信息作者:作者名.txt
|
||||
val m1 = Pattern
|
||||
.compile("(.*?)《([^《》]+)》(.*)")
|
||||
.matcher(tempFileName)
|
||||
//匹配 书名 作者:作者名.txt
|
||||
val m2 = Pattern
|
||||
.compile("(^)(.+) 作者:(.+)$")
|
||||
.matcher(tempFileName)
|
||||
|
||||
(m1.takeIf { m1.find() } ?: m2.takeIf { m2.find() }).run {
|
||||
if (this is Matcher) {
|
||||
//按默认格式将文件名分解成书名、作者名
|
||||
for (pattern in nameAuthorPatterns) {
|
||||
pattern.matcher(tempFileName).takeIf { it.find() }?.run {
|
||||
name = group(2)!!
|
||||
author = BookHelp.formatBookAuthor((group(1) ?: "") + (group(3) ?: ""))
|
||||
} else if (!AppConfig.bookImportFileName.isNullOrBlank()) {
|
||||
try {
|
||||
//在脚本中定义如何分解文件名成书名、作者名
|
||||
val jsonStr = AppConst.SCRIPT_ENGINE.eval(
|
||||
//在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获
|
||||
AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})",
|
||||
//将文件名注入到脚步的src变量中
|
||||
SimpleBindings().also { it["src"] = tempFileName }
|
||||
).toString()
|
||||
val bookMess = GSON.fromJsonObject<HashMap<String, String>>(jsonStr)
|
||||
.getOrThrow() ?: HashMap()
|
||||
name = bookMess["name"] ?: tempFileName
|
||||
author = bookMess["author"]?.takeIf { it.length != tempFileName.length } ?: ""
|
||||
} catch (e: Exception) {
|
||||
name = BookHelp.formatBookName(tempFileName)
|
||||
author = BookHelp.formatBookAuthor(tempFileName.replace(name, ""))
|
||||
.takeIf { it.length != tempFileName.length } ?: ""
|
||||
}
|
||||
} else {
|
||||
val group1 = group(1) ?: ""
|
||||
val group3 = group(3) ?: ""
|
||||
author = BookHelp.formatBookAuthor(group1 + group3)
|
||||
return Pair(name, author)
|
||||
}
|
||||
}
|
||||
if (!AppConfig.bookImportFileName.isNullOrBlank()) {
|
||||
try {
|
||||
//在脚本中定义如何分解文件名成书名、作者名
|
||||
val jsonStr = AppConst.SCRIPT_ENGINE.eval(
|
||||
//在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获
|
||||
AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})",
|
||||
//将文件名注入到脚步的src变量中
|
||||
SimpleBindings().also { it["src"] = tempFileName }
|
||||
).toString()
|
||||
val bookMess = GSON.fromJsonObject<HashMap<String, String>>(jsonStr)
|
||||
.getOrThrow() ?: HashMap()
|
||||
name = bookMess["name"] ?: tempFileName
|
||||
author = bookMess["author"]?.takeIf { it.length != tempFileName.length } ?: ""
|
||||
} catch (e: Exception) {
|
||||
name = BookHelp.formatBookName(tempFileName)
|
||||
author = BookHelp.formatBookAuthor(tempFileName.replace(name, ""))
|
||||
.takeIf { it.length != tempFileName.length } ?: ""
|
||||
}
|
||||
|
||||
} else {
|
||||
name = BookHelp.formatBookName(tempFileName)
|
||||
author = BookHelp.formatBookAuthor(tempFileName.replace(name, ""))
|
||||
.takeIf { it.length != tempFileName.length } ?: ""
|
||||
}
|
||||
return Pair(name, author)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class AudioPlayService : BaseService(),
|
||||
companion object {
|
||||
var isRun = false
|
||||
private set
|
||||
var pause = false
|
||||
var pause = true
|
||||
private set
|
||||
var timeMinute: Int = 0
|
||||
private set
|
||||
@@ -59,11 +59,10 @@ class AudioPlayService : BaseService(),
|
||||
private val exoPlayer: ExoPlayer by lazy {
|
||||
ExoPlayer.Builder(this).build()
|
||||
}
|
||||
private var title: String = ""
|
||||
private var subtitle: String = ""
|
||||
private var mediaSessionCompat: MediaSessionCompat? = null
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
private var position = 0
|
||||
private var audioFocusLossTransient = false
|
||||
private var position = AudioPlay.book?.durChapterPos ?: 0
|
||||
private var dsJob: Job? = null
|
||||
private var upPlayProgressJob: Job? = null
|
||||
private var playSpeed: Float = 1f
|
||||
@@ -76,18 +75,16 @@ class AudioPlayService : BaseService(),
|
||||
initMediaSession()
|
||||
initBroadcastReceiver()
|
||||
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
|
||||
doDs()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.action?.let { action ->
|
||||
when (action) {
|
||||
IntentAction.play -> {
|
||||
AudioPlay.book?.let {
|
||||
title = it.name
|
||||
subtitle = AudioPlay.durChapter?.title ?: ""
|
||||
position = it.durChapterPos
|
||||
loadContent()
|
||||
}
|
||||
pause = false
|
||||
position = AudioPlay.book?.durChapterPos ?: 0
|
||||
loadContent()
|
||||
}
|
||||
IntentAction.pause -> pause(true)
|
||||
IntentAction.resume -> resume()
|
||||
@@ -171,6 +168,10 @@ class AudioPlayService : BaseService(),
|
||||
private fun resume() {
|
||||
try {
|
||||
pause = false
|
||||
if (url.isEmpty()) {
|
||||
loadContent()
|
||||
return
|
||||
}
|
||||
if (!exoPlayer.isPlaying) {
|
||||
exoPlayer.play()
|
||||
}
|
||||
@@ -260,11 +261,11 @@ class AudioPlayService : BaseService(),
|
||||
}
|
||||
|
||||
private fun addTimer() {
|
||||
if (timeMinute == 60) {
|
||||
if (timeMinute == 180) {
|
||||
timeMinute = 0
|
||||
} else {
|
||||
timeMinute += 10
|
||||
if (timeMinute > 60) timeMinute = 60
|
||||
if (timeMinute > 180) timeMinute = 180
|
||||
}
|
||||
doDs()
|
||||
}
|
||||
@@ -273,7 +274,7 @@ class AudioPlayService : BaseService(),
|
||||
* 定时
|
||||
*/
|
||||
private fun doDs() {
|
||||
postEvent(EventBus.TTS_DS, timeMinute)
|
||||
postEvent(EventBus.AUDIO_DS, timeMinute)
|
||||
upNotification()
|
||||
dsJob?.cancel()
|
||||
dsJob = launch {
|
||||
@@ -287,7 +288,7 @@ class AudioPlayService : BaseService(),
|
||||
AudioPlay.stop(this@AudioPlayService)
|
||||
}
|
||||
}
|
||||
postEvent(EventBus.TTS_DS, timeMinute)
|
||||
postEvent(EventBus.AUDIO_DS, timeMinute)
|
||||
upNotification()
|
||||
}
|
||||
}
|
||||
@@ -360,7 +361,6 @@ class AudioPlayService : BaseService(),
|
||||
*/
|
||||
private fun contentLoadFinish(chapter: BookChapter, content: String) {
|
||||
if (chapter.index == AudioPlay.book?.durChapterIndex) {
|
||||
subtitle = chapter.title
|
||||
url = content
|
||||
play()
|
||||
}
|
||||
@@ -426,13 +426,18 @@ class AudioPlayService : BaseService(),
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
// 重新获得焦点, 可做恢复播放,恢复后台音量的操作
|
||||
audioFocusLossTransient = false
|
||||
if (!pause) resume()
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
// 永久丢失焦点除非重新主动获取,这种情况是被其他播放器抢去了焦点, 为避免与其他播放器混音,可将音乐暂停
|
||||
if (audioFocusLossTransient) {
|
||||
pause(true)
|
||||
}
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
// 暂时丢失焦点,这种情况是被其他应用申请了短暂的焦点,可压低后台音量
|
||||
audioFocusLossTransient = true
|
||||
if (!pause) pause(false)
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
@@ -454,9 +459,9 @@ class AudioPlayService : BaseService(),
|
||||
)
|
||||
else -> getString(R.string.audio_play_t)
|
||||
}
|
||||
nTitle += ": $title"
|
||||
var nSubtitle = subtitle
|
||||
if (subtitle.isEmpty()) {
|
||||
nTitle += ": ${AudioPlay.book?.name}"
|
||||
var nSubtitle = AudioPlay.durChapter?.title
|
||||
if (nSubtitle.isNullOrEmpty()) {
|
||||
nSubtitle = getString(R.string.audio_play_s)
|
||||
}
|
||||
val builder = NotificationCompat
|
||||
|
||||
@@ -206,7 +206,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
*/
|
||||
@Synchronized
|
||||
private fun doDs() {
|
||||
postEvent(EventBus.TTS_DS, timeMinute)
|
||||
postEvent(EventBus.READ_ALOUD_DS, timeMinute)
|
||||
upNotification()
|
||||
dsJob?.cancel()
|
||||
dsJob = launch {
|
||||
@@ -220,7 +220,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
ReadAloud.stop(this@BaseReadAloudService)
|
||||
}
|
||||
}
|
||||
postEvent(EventBus.TTS_DS, timeMinute)
|
||||
postEvent(EventBus.READ_ALOUD_DS, timeMinute)
|
||||
upNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.ui.book.audio
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.os.Build
|
||||
@@ -8,6 +9,7 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.SeekBar
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.EventBus
|
||||
@@ -25,6 +27,7 @@ import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.toc.TocActivityResult
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
@@ -42,6 +45,7 @@ class AudioPlayActivity :
|
||||
override val viewModel by viewModels<AudioPlayViewModel>()
|
||||
private var menu: Menu? = null
|
||||
private var adjustProgress = false
|
||||
private val timerViewState = mutableStateOf(false)
|
||||
private val progressTimeFormat by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
SimpleDateFormat("mm:ss", Locale.getDefault())
|
||||
@@ -153,7 +157,19 @@ class AudioPlayActivity :
|
||||
AudioPlay.adjustSpeed(this@AudioPlayActivity, -0.1f)
|
||||
}
|
||||
binding.ivTimer.setOnClickListener {
|
||||
AudioPlay.addTimer(this@AudioPlayActivity)
|
||||
if (AudioPlayService.isRun) {
|
||||
timerViewState.value = true
|
||||
} else {
|
||||
toastOnUi(R.string.cannot_timed_non_playback)
|
||||
}
|
||||
}
|
||||
binding.composeView.setContent {
|
||||
AppTheme {
|
||||
TimerDialog(
|
||||
state = timerViewState,
|
||||
binding.ivTimer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +226,7 @@ class AudioPlayActivity :
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun observeLiveBus() {
|
||||
observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
|
||||
if (it) {
|
||||
@@ -239,6 +256,10 @@ class AudioPlayActivity :
|
||||
binding.tvSpeed.text = String.format("%.1fX", it)
|
||||
binding.tvSpeed.visible()
|
||||
}
|
||||
observeEventSticky<Int>(EventBus.AUDIO_DS) {
|
||||
binding.tvTimer.text = "${it}m"
|
||||
binding.tvTimer.visible(it > 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.legado.app.ui.book.audio
|
||||
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Slider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import io.legado.app.model.AudioPlay
|
||||
import io.legado.app.service.AudioPlayService
|
||||
|
||||
|
||||
@Composable
|
||||
fun TimerDialog(state: MutableState<Boolean>, parent: View) {
|
||||
val intOffset = IntArray(2)
|
||||
parent.getLocationInWindow(intOffset)
|
||||
if (state.value) {
|
||||
val timeMinute = remember {
|
||||
mutableStateOf(AudioPlayService.timeMinute)
|
||||
}
|
||||
Dialog(onDismissRequest = { state.value = false }) {
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Slider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
value = timeMinute.value.toFloat(), onValueChange = {
|
||||
timeMinute.value = it.toInt()
|
||||
AudioPlay.setTimer(it.toInt())
|
||||
},
|
||||
valueRange = 0f..180f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,10 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
||||
}.map { docList ->
|
||||
when (sort) {
|
||||
2 -> docList.sortedWith(
|
||||
compareBy({ !it.isDir }, { it.lastModified }, { it.name })
|
||||
compareBy({ !it.isDir }, { -it.lastModified }, { it.name })
|
||||
)
|
||||
1 -> docList.sortedWith(
|
||||
compareBy({ !it.isDir }, { it.size }, { it.name })
|
||||
compareBy({ !it.isDir }, { -it.size }, { it.name })
|
||||
)
|
||||
else -> docList.sortedWith(
|
||||
compareBy({ !it.isDir }, { it.name })
|
||||
|
||||
@@ -168,20 +168,24 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
book: Book,
|
||||
alertSync: ((progress: BookProgress) -> Unit)? = null
|
||||
) {
|
||||
if (AppConfig.syncBookProgress)
|
||||
execute {
|
||||
execute {
|
||||
if (AppWebDav.syncBookProgress) {
|
||||
AppWebDav.getBookProgress(book)
|
||||
}.onSuccess {
|
||||
it?.let { progress ->
|
||||
if (progress.durChapterIndex < book.durChapterIndex ||
|
||||
(progress.durChapterIndex == book.durChapterIndex && progress.durChapterPos < book.durChapterPos)
|
||||
) {
|
||||
alertSync?.invoke(progress)
|
||||
} else {
|
||||
ReadBook.setProgress(progress)
|
||||
}
|
||||
}
|
||||
?: throw NoStackTraceException("没有进度")
|
||||
} else {
|
||||
throw NoStackTraceException("进度同步未启用")
|
||||
}
|
||||
}.onSuccess { progress ->
|
||||
if (progress.durChapterIndex < book.durChapterIndex ||
|
||||
(progress.durChapterIndex == book.durChapterIndex
|
||||
&& progress.durChapterPos < book.durChapterPos)
|
||||
) {
|
||||
alertSync?.invoke(progress)
|
||||
} else {
|
||||
ReadBook.setProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -184,7 +184,7 @@ class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) {
|
||||
|
||||
override fun observeLiveBus() {
|
||||
observeEvent<Int>(EventBus.ALOUD_STATE) { upPlayState() }
|
||||
observeEvent<Int>(EventBus.TTS_DS) { binding.seekTimer.progress = it }
|
||||
observeEvent<Int>(EventBus.READ_ALOUD_DS) { binding.seekTimer.progress = it }
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
|
||||
Reference in New Issue
Block a user