fix: 朗读相关

This commit is contained in:
HapeLee committed 2026-06-09 02:48:12 +08:00
1 parent f00da95724
commit e28be0301b
14 files changed
+703 -525

No files matched your search

@@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.emptyPreferences
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.readConfig.ReadConfig
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -42,41 +43,53 @@ class ReadAloudSettingsRepository(
preferences.toReadAloudPreferences() preferences.toReadAloudPreferences()
} }
suspend fun setIgnoreAudioFocus(value: Boolean) = suspend fun setIgnoreAudioFocus(value: Boolean) {
settingsRepository.putBoolean(PreferKey.ignoreAudioFocus, value) ReadConfig.ignoreAudioFocus = value
}
suspend fun setMediaButtonOnExit(value: Boolean) = suspend fun setMediaButtonOnExit(value: Boolean) {
settingsRepository.putBoolean(PreferKey.mediaButtonOnExit, value) settingsRepository.putBoolean(PreferKey.mediaButtonOnExit, value)
}
suspend fun setReadAloudByMediaButton(value: Boolean) = suspend fun setReadAloudByMediaButton(value: Boolean) {
settingsRepository.putBoolean(PreferKey.readAloudByMediaButton, value) settingsRepository.putBoolean(PreferKey.readAloudByMediaButton, value)
}
suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) = suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) {
settingsRepository.putBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value) ReadConfig.pauseReadAloudWhilePhoneCalls = value
}
suspend fun setReadAloudWakeLock(value: Boolean) = suspend fun setReadAloudWakeLock(value: Boolean) {
settingsRepository.putBoolean(PreferKey.readAloudWakeLock, value) ReadConfig.readAloudWakeLock = value
}
suspend fun setMediaButtonPerNext(value: Boolean) = suspend fun setMediaButtonPerNext(value: Boolean) {
settingsRepository.putBoolean(KEY_MEDIA_BUTTON_PER_NEXT, value) ReadConfig.mediaButtonPerNext = value
}
suspend fun setReadAloudByPage(value: Boolean) = suspend fun setReadAloudByPage(value: Boolean) {
settingsRepository.putBoolean(PreferKey.readAloudByPage, value) ReadConfig.readAloudByPage = value
}
suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) = suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) {
settingsRepository.putBoolean(PreferKey.systemMediaControlCompatibilityChange, value) ReadConfig.systemMediaControlCompatibilityChange = value
}
suspend fun setStreamReadAloudAudio(value: Boolean) = suspend fun setStreamReadAloudAudio(value: Boolean) {
settingsRepository.putBoolean(PreferKey.streamReadAloudAudio, value) ReadConfig.streamReadAloudAudio = value
}
suspend fun setTtsTimer(value: Int) = suspend fun setTtsTimer(value: Int) {
settingsRepository.putInt(PreferKey.ttsTimer, value.coerceIn(0, 180)) ReadConfig.ttsTimer = value.coerceIn(0, 180)
}
suspend fun setTtsFollowSys(value: Boolean) = suspend fun setTtsFollowSys(value: Boolean) {
settingsRepository.putBoolean(PreferKey.ttsFollowSys, value) ReadConfig.ttsFollowSys = value
}
suspend fun setTtsSpeechRate(value: Int) = suspend fun setTtsSpeechRate(value: Int) {
settingsRepository.putInt(PreferKey.ttsSpeechRate, value.coerceIn(0, 80)) ReadConfig.ttsSpeechRate = value.coerceIn(0, 80)
}
private fun Preferences.toReadAloudPreferences(): ReadAloudPreferences { private fun Preferences.toReadAloudPreferences(): ReadAloudPreferences {
return ReadAloudPreferences( return ReadAloudPreferences(
@@ -376,9 +376,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
} }
var ttsFlowSys: Boolean var ttsFlowSys: Boolean
get() = appCtx.getPrefBoolean(PreferKey.ttsFollowSys, true) get() = ReadConfig.ttsFollowSys
set(value) { set(value) {
appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value) ReadConfig.ttsFollowSys = value
} }
val noAnimScrollPage: Boolean val noAnimScrollPage: Boolean
@@ -387,15 +387,15 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
const val defaultSpeechRate = 5 const val defaultSpeechRate = 5
var ttsSpeechRate: Int var ttsSpeechRate: Int
get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, defaultSpeechRate) get() = ReadConfig.ttsSpeechRate
set(value) { set(value) {
appCtx.putPrefInt(PreferKey.ttsSpeechRate, value) ReadConfig.ttsSpeechRate = value
} }
var ttsTimer: Int var ttsTimer: Int
get() = appCtx.getPrefInt(PreferKey.ttsTimer, 0) get() = ReadConfig.ttsTimer
set(value) { set(value) {
appCtx.putPrefInt(PreferKey.ttsTimer, value) ReadConfig.ttsTimer = value
} }
val speechRatePlay: Int get() = if (ttsFlowSys) defaultSpeechRate else ttsSpeechRate val speechRatePlay: Int get() = if (ttsFlowSys) defaultSpeechRate else ttsSpeechRate
@@ -604,11 +604,17 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val showAddToShelfAlert get() = OtherConfig.showAddToShelfAlert val showAddToShelfAlert get() = OtherConfig.showAddToShelfAlert
val ignoreAudioFocus get() = appCtx.getPrefBoolean(PreferKey.ignoreAudioFocus, false) var ignoreAudioFocus
get() = ReadConfig.ignoreAudioFocus
set(value) {
ReadConfig.ignoreAudioFocus = value
}
var pauseReadAloudWhilePhoneCalls var pauseReadAloudWhilePhoneCalls
get() = appCtx.getPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, false) get() = ReadConfig.pauseReadAloudWhilePhoneCalls
set(value) = appCtx.putPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value) set(value) {
ReadConfig.pauseReadAloudWhilePhoneCalls = value
}
val onlyLatestBackup get() = BackupConfig.onlyLatestBackup val onlyLatestBackup get() = BackupConfig.onlyLatestBackup
@@ -618,7 +624,11 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val updateToVariant get() = OtherConfig.updateToVariant val updateToVariant get() = OtherConfig.updateToVariant
val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false) var streamReadAloudAudio
get() = ReadConfig.streamReadAloudAudio
set(value) {
ReadConfig.streamReadAloudAudio = value
}
val doublePageHorizontal: String? val doublePageHorizontal: String?
get() = ReadConfig.doubleHorizontalPage get() = ReadConfig.doubleHorizontalPage
@@ -940,9 +950,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
} }
var systemMediaControlCompatibilityChange: Boolean var systemMediaControlCompatibilityChange: Boolean
get() = appCtx.getPrefBoolean(PreferKey.systemMediaControlCompatibilityChange, true) get() = ReadConfig.systemMediaControlCompatibilityChange
set(value) { set(value) {
appCtx.putPrefBoolean(PreferKey.systemMediaControlCompatibilityChange, value) ReadConfig.systemMediaControlCompatibilityChange = value
} }
var isPredictiveBackEnabled: Boolean var isPredictiveBackEnabled: Boolean
@@ -7,36 +7,25 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PageAnim import io.legado.app.constant.PageAnim
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.ReadMenuBlurMode import io.legado.app.constant.ReadMenuBlurMode
import io.legado.app.constant.ReadMenuBlurStyle import io.legado.app.constant.ReadMenuBlurStyle
import io.legado.app.data.repository.ReadPreferences import androidx.compose.runtime.State
import io.legado.app.data.repository.ReadStyleRepository import io.legado.app.data.repository.ReadStyleRepository
import io.legado.app.data.repository.dataStore
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.config.PrefDelegate
import io.legado.app.ui.config.prefDelegate
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getMeanColor import io.legado.app.utils.getMeanColor
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefFloat
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString import io.legado.app.utils.hexString
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import splitties.init.appCtx import splitties.init.appCtx
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/** /**
* 阅读界面配置 * 阅读界面配置
@@ -46,6 +35,40 @@ import java.io.InputStream
object ReadBookConfig { object ReadBookConfig {
private val readStyleRepository = ReadStyleRepository() private val readStyleRepository = ReadStyleRepository()
// region prefDelegate helpers
/** 读写时自动 coerceIn 的 prefDelegate */
private fun <T : Comparable<T>> clampedPrefDelegate(
key: String,
defaultValue: T,
range: ClosedRange<T>,
): PrefDelegate<T> {
val raw = prefDelegate(key, defaultValue)
return object : PrefDelegate<T> {
override val state: State<T> get() = raw.state
override fun dispose() = raw.dispose()
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
raw.getValue(thisRef, property).coerceIn(range)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
raw.setValue(thisRef, property, value.coerceIn(range))
}
}
/** Map<String,String> 存储为 JSON String 的 prefDelegate */
private class PrefDelegateMap(
key: String,
private val parse: (String?) -> Map<String, String>,
private val encode: (Map<String, String>) -> String,
) : ReadWriteProperty<Any?, Map<String, String>> {
private val raw = prefDelegate<String?>(key, null)
override fun getValue(thisRef: Any?, property: KProperty<*>): Map<String, String> =
parse(raw.getValue(thisRef, property))
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Map<String, String>) =
raw.setValue(thisRef, property, encode(value))
}
// endregion
// region Tip position constants // region Tip position constants
const val tipNone = 0 const val tipNone = 0
const val tipChapterTitle = 1 const val tipChapterTitle = 1
@@ -94,23 +117,6 @@ object ReadBookConfig {
initShareConfig() initShareConfig()
} }
/**
* 从 DataStore 读取 Int 值,DS 无值时回退到 SharedPreferences。
* 与 PrefDelegate.readFromDs() 逻辑一致。
*/
private fun readIntFromDataStore(key: String, defaultValue: Int): Int {
return runBlocking(Dispatchers.IO) {
try {
appCtx.dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { it[intPreferencesKey(key)] }
.first() ?: defaultValue
} catch (_: Exception) {
appCtx.getPrefInt(key, defaultValue)
}
}
}
@Synchronized @Synchronized
fun getConfig(index: Int): Config { fun getConfig(index: Int): Config {
if (configList.size < 5) { if (configList.size < 5) {
@@ -179,195 +185,115 @@ object ReadBookConfig {
} }
} }
// Runtime compatibility snapshot. New write/read flows should use ReadSettingsRepository. // region Scalar properties (prefDelegate — auto-syncs SP + DataStore)
private var readBodyToLhValue = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
var readBodyToLh: Boolean
get() = readBodyToLhValue
set(value) {
readBodyToLhValue = value
}
private var autoReadSpeedValue = appCtx.getPrefInt(PreferKey.autoReadSpeed, 10) var readBodyToLh by prefDelegate(PreferKey.readBodyToLh, true)
var autoReadSpeed: Int var autoReadSpeed by prefDelegate(PreferKey.autoReadSpeed, 10)
get() = autoReadSpeedValue var readStyleSelect by prefDelegate(PreferKey.readStyleSelect, 0)
set(value) { var comicStyleSelect by prefDelegate(PreferKey.comicStyleSelect, 0)
autoReadSpeedValue = value var shareLayout by prefDelegate(PreferKey.shareLayout, false)
appCtx.putPrefInt(PreferKey.autoReadSpeed, value) var textFullJustify by prefDelegate(PreferKey.textFullJustify, true)
} var textBottomJustify by prefDelegate(PreferKey.textBottomJustify, true)
var hideStatusBar by prefDelegate(PreferKey.hideStatusBar, false)
var hideNavigationBar by prefDelegate(PreferKey.hideNavigationBar, false)
var useZhLayout by prefDelegate(PreferKey.useZhLayout, false)
var readMenuIconShowText by prefDelegate(PreferKey.readMenuIconShowText, true)
var readMenuFloatingBottomBar by prefDelegate(PreferKey.readMenuFloatingBottomBar, false)
var readMenuTopBarLiquidGlassButtons by prefDelegate(PreferKey.readMenuTopBarLiquidGlassButtons, false)
var readMenuBottomBarLiquidGlassButtons by prefDelegate(PreferKey.readMenuBottomBarLiquidGlassButtons, false)
var readMenuBorderColor by prefDelegate(PreferKey.readMenuBorderColor, 0)
var readMenuBorderColorNight by prefDelegate(PreferKey.readMenuBorderColorNight, 0)
var showTitleBarIcons by prefDelegate(PreferKey.showTitleBarIcons, true)
var readSliderMode by prefDelegate(PreferKey.readSliderMode, "0")
var styleSelect: Int var styleSelect: Int
get() = if (isComic) comicStyleSelect else readStyleSelect get() = if (isComic) comicStyleSelect else readStyleSelect
set(value) { set(value) {
if (isComic) { if (isComic) comicStyleSelect = value else readStyleSelect = value
comicStyleSelect = value
} else {
readStyleSelect = value
}
} }
private var readStyleSelectValue = readIntFromDataStore(PreferKey.readStyleSelect, 0) // endregion
var readStyleSelect: Int
get() = readStyleSelectValue
set(value) {
readStyleSelectValue = value
if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) {
appCtx.putPrefInt(PreferKey.readStyleSelect, value)
}
}
private var comicStyleSelectValue = // region coerceIn properties (clampedPrefDelegate)
readIntFromDataStore(PreferKey.comicStyleSelect, readStyleSelect)
var comicStyleSelect: Int
get() = comicStyleSelectValue
set(value) {
comicStyleSelectValue = value
if (appCtx.getPrefInt(PreferKey.comicStyleSelect) != value) {
appCtx.putPrefInt(PreferKey.comicStyleSelect, value)
}
}
private var shareLayoutValue = appCtx.getPrefBoolean(PreferKey.shareLayout) var readMenuColorMode by clampedPrefDelegate(PreferKey.readMenuColorMode, 1, 0..1)
var shareLayout: Boolean var readMenuIconStyle by clampedPrefDelegate(PreferKey.readMenuIconStyle, 0, 0..2)
get() = shareLayoutValue var readMenuIconItemsPerRow by clampedPrefDelegate(PreferKey.readMenuIconItemsPerRow, 5, 2..8)
set(value) { var readMenuIconRowCount by clampedPrefDelegate(PreferKey.readMenuIconRowCount, 1, 1..2)
shareLayoutValue = value var readMenuBottomCornerRadius by clampedPrefDelegate(PreferKey.readMenuBottomCornerRadius, 0, 0..32)
if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) { var readMenuTopBarBlurMode by clampedPrefDelegate(PreferKey.readMenuTopBarBlurMode, ReadMenuBlurMode.None, 0..2)
appCtx.putPrefBoolean(PreferKey.shareLayout, value) var readMenuBottomBarBlurMode by clampedPrefDelegate(PreferKey.readMenuBottomBarBlurMode, ReadMenuBlurMode.None, 0..2)
} var readMenuTopBarBlurStyle by clampedPrefDelegate(PreferKey.readMenuTopBarBlurStyle, ReadMenuBlurStyle.Progressive, 0..1)
} var readMenuBottomBarBlurStyle by clampedPrefDelegate(PreferKey.readMenuBottomBarBlurStyle, ReadMenuBlurStyle.Solid, 0..1)
var readMenuBlurRadius by clampedPrefDelegate(PreferKey.readMenuBlurRadius, 24, 0..32)
var readMenuBlurAlpha by clampedPrefDelegate(PreferKey.readMenuBlurAlpha, 60, 0..100)
var readMenuLensRadius by clampedPrefDelegate(PreferKey.readMenuLensRadius, 24f, 0f..48f)
var readMenuBorderWidth by clampedPrefDelegate(PreferKey.readMenuBorderWidth, 0, 0..4)
var titleBarIconPosition by clampedPrefDelegate(PreferKey.titleBarIconPosition, 0, 0..3)
private var textFullJustifyValue = appCtx.getPrefBoolean(PreferKey.textFullJustify, true) // endregion
private var textBottomJustifyValue = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true)
private var hideStatusBarValue = appCtx.getPrefBoolean(PreferKey.hideStatusBar)
private var hideNavigationBarValue = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
private var useZhLayoutValue = appCtx.getPrefBoolean(PreferKey.useZhLayout)
private var readMenuBgColorValue = appCtx.getPrefInt(PreferKey.readMenuBgColor)
private var readMenuAccentColorValue = appCtx.getPrefInt(PreferKey.readMenuAccentColor)
private var readMenuContainerColorValue = appCtx.getPrefInt(PreferKey.readMenuContainerColor)
private var readMenuBgColorNightValue = appCtx.getPrefInt(PreferKey.readMenuBgColorNight)
private var readMenuAccentColorNightValue = appCtx.getPrefInt(PreferKey.readMenuAccentColorNight)
private var readMenuContainerColorNightValue = appCtx.getPrefInt(PreferKey.readMenuContainerColorNight)
private var readMenuColorModeValue = appCtx.getPrefInt(PreferKey.readMenuColorMode, 1)
private var readMenuIconShowTextValue = appCtx.getPrefBoolean(PreferKey.readMenuIconShowText, true)
private var readMenuIconStyleValue = appCtx.getPrefInt(PreferKey.readMenuIconStyle)
private var readMenuIconItemsPerRowValue = appCtx.getPrefInt(PreferKey.readMenuIconItemsPerRow, 5)
private var readMenuIconRowCountValue = appCtx.getPrefInt(PreferKey.readMenuIconRowCount, 1)
private var readMenuBottomCornerRadiusValue = appCtx.getPrefInt(PreferKey.readMenuBottomCornerRadius)
private var readMenuFloatingBottomBarValue = appCtx.getPrefBoolean(PreferKey.readMenuFloatingBottomBar)
private var readMenuTopBarBlurModeValue = appCtx.getPrefInt(
PreferKey.readMenuTopBarBlurMode,
ReadMenuBlurMode.None
)
private var readMenuBottomBarBlurModeValue = appCtx.getPrefInt(
PreferKey.readMenuBottomBarBlurMode,
ReadMenuBlurMode.None
)
private var readMenuTopBarLiquidGlassButtonsValue = appCtx.getPrefBoolean(
PreferKey.readMenuTopBarLiquidGlassButtons
)
private var readMenuBottomBarLiquidGlassButtonsValue = appCtx.getPrefBoolean(
PreferKey.readMenuBottomBarLiquidGlassButtons
)
private var readMenuTopBarBlurStyleValue = appCtx.getPrefInt(
PreferKey.readMenuTopBarBlurStyle,
ReadMenuBlurStyle.Progressive
)
private var readMenuBottomBarBlurStyleValue = appCtx.getPrefInt(
PreferKey.readMenuBottomBarBlurStyle,
ReadMenuBlurStyle.Solid
)
private var readMenuBlurRadiusValue = appCtx.getPrefInt(PreferKey.readMenuBlurRadius, 24)
private var readMenuBlurAlphaValue = appCtx.getPrefInt(PreferKey.readMenuBlurAlpha, 60)
private var readMenuLensRadiusValue = appCtx.getPrefFloat(PreferKey.readMenuLensRadius, 24f)
private var readMenuBorderWidthValue = appCtx.getPrefInt(PreferKey.readMenuBorderWidth)
private var readMenuBorderColorValue = appCtx.getPrefInt(PreferKey.readMenuBorderColor)
private var readMenuBorderColorNightValue = appCtx.getPrefInt(PreferKey.readMenuBorderColorNight)
private var readMenuCustomIconsValue =
parseReadMenuCustomIcons(appCtx.getPrefString(PreferKey.readMenuCustomIcons))
private var titleBarCustomIconsValue =
parseReadMenuCustomIcons(appCtx.getPrefString(PreferKey.titleBarCustomIcons))
private var titleBarIconPositionValue = appCtx.getPrefInt(PreferKey.titleBarIconPosition)
private var showTitleBarIconsValue = appCtx.getPrefBoolean(PreferKey.showTitleBarIcons, true)
private var readSliderModeValue = appCtx.getPrefString(PreferKey.readSliderMode) ?: "0"
fun syncPreferences(preferences: ReadPreferences) { // region Color properties (prefDelegate backing + custom fallback getter)
readBodyToLhValue = preferences.readBodyToLh
autoReadSpeedValue = preferences.autoReadSpeed private val readMenuBgColorDelegate = prefDelegate(PreferKey.readMenuBgColor, 0)
readStyleSelectValue = preferences.readStyleSelect var readMenuBgColor: Int
comicStyleSelectValue = preferences.comicStyleSelect get() = readMenuBgColorDelegate.getValue(this, ::readMenuBgColor)
shareLayoutValue = preferences.shareLayout .takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = false)
textFullJustifyValue = preferences.textFullJustify set(value) = readMenuBgColorDelegate.setValue(this, ::readMenuBgColor, value)
textBottomJustifyValue = preferences.textBottomJustify
hideStatusBarValue = preferences.hideStatusBar private val readMenuAccentColorDelegate = prefDelegate(PreferKey.readMenuAccentColor, 0)
hideNavigationBarValue = preferences.hideNavigationBar var readMenuAccentColor: Int
useZhLayoutValue = preferences.useZhLayout get() = readMenuAccentColorDelegate.getValue(this, ::readMenuAccentColor)
readMenuBgColorValue = preferences.readMenuBgColor .takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = false)
readMenuAccentColorValue = preferences.readMenuAccentColor set(value) = readMenuAccentColorDelegate.setValue(this, ::readMenuAccentColor, value)
readMenuContainerColorValue = preferences.readMenuContainerColor
readMenuBgColorNightValue = preferences.readMenuBgColorNight private val readMenuContainerColorDelegate = prefDelegate(PreferKey.readMenuContainerColor, 0)
readMenuAccentColorNightValue = preferences.readMenuAccentColorNight var readMenuContainerColor: Int
readMenuContainerColorNightValue = preferences.readMenuContainerColorNight get() = readMenuContainerColorDelegate.getValue(this, ::readMenuContainerColor)
readMenuColorModeValue = preferences.readMenuColorMode .takeIf { it != 0 } ?: readMenuBgColor
readMenuIconShowTextValue = preferences.readMenuIconShowText set(value) = readMenuContainerColorDelegate.setValue(this, ::readMenuContainerColor, value)
readMenuIconStyleValue = preferences.readMenuIconStyle
readMenuIconItemsPerRowValue = preferences.readMenuIconItemsPerRow private val readMenuBgColorNightDelegate = prefDelegate(PreferKey.readMenuBgColorNight, 0)
readMenuIconRowCountValue = preferences.readMenuIconRowCount var readMenuBgColorNight: Int
readMenuBottomCornerRadiusValue = preferences.readMenuBottomCornerRadius get() = readMenuBgColorNightDelegate.getValue(this, ::readMenuBgColorNight)
readMenuFloatingBottomBarValue = preferences.readMenuFloatingBottomBar .takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = true)
readMenuTopBarBlurModeValue = preferences.readMenuTopBarBlurMode set(value) = readMenuBgColorNightDelegate.setValue(this, ::readMenuBgColorNight, value)
readMenuBottomBarBlurModeValue = preferences.readMenuBottomBarBlurMode
readMenuTopBarLiquidGlassButtonsValue = preferences.readMenuTopBarLiquidGlassButtons private val readMenuAccentColorNightDelegate = prefDelegate(PreferKey.readMenuAccentColorNight, 0)
readMenuBottomBarLiquidGlassButtonsValue = preferences.readMenuBottomBarLiquidGlassButtons var readMenuAccentColorNight: Int
readMenuTopBarBlurStyleValue = preferences.readMenuTopBarBlurStyle get() = readMenuAccentColorNightDelegate.getValue(this, ::readMenuAccentColorNight)
readMenuBottomBarBlurStyleValue = preferences.readMenuBottomBarBlurStyle .takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = true)
readMenuBlurRadiusValue = preferences.readMenuBlurRadius set(value) = readMenuAccentColorNightDelegate.setValue(this, ::readMenuAccentColorNight, value)
readMenuBlurAlphaValue = preferences.readMenuBlurAlpha
readMenuLensRadiusValue = preferences.readMenuLensRadius private val readMenuContainerColorNightDelegate = prefDelegate(PreferKey.readMenuContainerColorNight, 0)
readMenuBorderWidthValue = preferences.readMenuBorderWidth var readMenuContainerColorNight: Int
readMenuBorderColorValue = preferences.readMenuBorderColor get() = readMenuContainerColorNightDelegate.getValue(this, ::readMenuContainerColorNight)
readMenuBorderColorNightValue = preferences.readMenuBorderColorNight .takeIf { it != 0 } ?: readMenuBgColorNight
readMenuCustomIconsValue = parseReadMenuCustomIcons(preferences.readMenuCustomIcons) set(value) = readMenuContainerColorNightDelegate.setValue(this, ::readMenuContainerColorNight, value)
titleBarCustomIconsValue = parseReadMenuCustomIcons(preferences.titleBarCustomIcons)
titleBarIconPositionValue = preferences.titleBarIconPosition // endregion
showTitleBarIconsValue = preferences.showTitleBarIcons
readSliderModeValue = preferences.readSliderMode // region Map properties (PrefDelegateMap — JSON string serialization)
fun encodeReadMenuCustomIcons(value: Map<String, String>): String {
return GSON.toJson(value.filterValues { it.isNotBlank() })
} }
var readMenuBgColor: Int private fun parseReadMenuCustomIcons(value: String?): Map<String, String> {
get() = readMenuBgColorValue.takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = false) if (value.isNullOrBlank()) return emptyMap()
set(value) { return GSON.fromJsonObject<Map<String, String>>(value).getOrNull()
readMenuBgColorValue = value ?.filterValues { it.isNotBlank() } ?: emptyMap()
} }
var readMenuAccentColor: Int var readMenuCustomIcons: Map<String, String> by PrefDelegateMap(
get() = readMenuAccentColorValue.takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = false) PreferKey.readMenuCustomIcons, ::parseReadMenuCustomIcons, ::encodeReadMenuCustomIcons,
set(value) { )
readMenuAccentColorValue = value
}
var readMenuContainerColor: Int var titleBarCustomIcons: Map<String, String> by PrefDelegateMap(
get() = readMenuContainerColorValue.takeIf { it != 0 } ?: readMenuBgColor PreferKey.titleBarCustomIcons, ::parseReadMenuCustomIcons, ::encodeReadMenuCustomIcons,
set(value) { )
readMenuContainerColorValue = value
}
var readMenuBgColorNight: Int // endregion
get() = readMenuBgColorNightValue.takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = true)
set(value) {
readMenuBgColorNightValue = value
}
var readMenuAccentColorNight: Int
get() = readMenuAccentColorNightValue.takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = true)
set(value) {
readMenuAccentColorNightValue = value
}
var readMenuContainerColorNight: Int
get() = readMenuContainerColorNightValue.takeIf { it != 0 } ?: readMenuBgColorNight
set(value) {
readMenuContainerColorNightValue = value
}
val resolvedMenuBgColor: Int val resolvedMenuBgColor: Int
get() { get() {
@@ -400,198 +326,15 @@ object ReadBookConfig {
val resolvedMenuContainerColor: Int val resolvedMenuContainerColor: Int
get() = if (ReadStyleResolver.isNightTheme()) readMenuContainerColorNight else readMenuContainerColor get() = if (ReadStyleResolver.isNightTheme()) readMenuContainerColorNight else readMenuContainerColor
var readMenuColorMode: Int
get() = readMenuColorModeValue.coerceIn(0, 1)
set(value) {
readMenuColorModeValue = value.coerceIn(0, 1)
}
var readMenuIconShowText: Boolean
get() = readMenuIconShowTextValue
set(value) {
readMenuIconShowTextValue = value
}
var readMenuIconStyle: Int
get() = readMenuIconStyleValue.coerceIn(0, 2)
set(value) {
readMenuIconStyleValue = value.coerceIn(0, 2)
}
var readMenuIconItemsPerRow: Int
get() = readMenuIconItemsPerRowValue.coerceIn(2, 8)
set(value) {
readMenuIconItemsPerRowValue = value.coerceIn(2, 8)
}
var readMenuIconRowCount: Int
get() = readMenuIconRowCountValue.coerceIn(1, 2)
set(value) {
readMenuIconRowCountValue = value.coerceIn(1, 2)
}
var readMenuBottomCornerRadius: Int
get() = readMenuBottomCornerRadiusValue.coerceIn(0, 32)
set(value) {
readMenuBottomCornerRadiusValue = value.coerceIn(0, 32)
}
var readMenuFloatingBottomBar: Boolean
get() = readMenuFloatingBottomBarValue
set(value) {
readMenuFloatingBottomBarValue = value
}
var readMenuTopBarBlurMode: Int
get() = readMenuTopBarBlurModeValue.coerceIn(0, 2)
set(value) {
readMenuTopBarBlurModeValue = value.coerceIn(0, 2)
}
var readMenuBottomBarBlurMode: Int
get() = readMenuBottomBarBlurModeValue.coerceIn(0, 2)
set(value) {
readMenuBottomBarBlurModeValue = value.coerceIn(0, 2)
}
var readMenuTopBarLiquidGlassButtons: Boolean
get() = readMenuTopBarLiquidGlassButtonsValue
set(value) {
readMenuTopBarLiquidGlassButtonsValue = value
}
var readMenuBottomBarLiquidGlassButtons: Boolean
get() = readMenuBottomBarLiquidGlassButtonsValue
set(value) {
readMenuBottomBarLiquidGlassButtonsValue = value
}
var readMenuTopBarBlurStyle: Int
get() = readMenuTopBarBlurStyleValue.coerceIn(0, 1)
set(value) {
readMenuTopBarBlurStyleValue = value.coerceIn(0, 1)
}
var readMenuBottomBarBlurStyle: Int
get() = readMenuBottomBarBlurStyleValue.coerceIn(0, 1)
set(value) {
readMenuBottomBarBlurStyleValue = value.coerceIn(0, 1)
}
var readMenuBlurRadius: Int
get() = readMenuBlurRadiusValue.coerceIn(0, 32)
set(value) {
readMenuBlurRadiusValue = value.coerceIn(0, 32)
}
var readMenuBlurAlpha: Int
get() = readMenuBlurAlphaValue.coerceIn(0, 100)
set(value) {
readMenuBlurAlphaValue = value.coerceIn(0, 100)
}
var readMenuLensRadius: Float
get() = readMenuLensRadiusValue.coerceIn(0f, 48f)
set(value) {
readMenuLensRadiusValue = value.coerceIn(0f, 48f)
}
var readMenuBorderWidth: Int
get() = readMenuBorderWidthValue.coerceIn(0, 4)
set(value) {
readMenuBorderWidthValue = value.coerceIn(0, 4)
}
var readMenuBorderColor: Int
get() = readMenuBorderColorValue
set(value) {
readMenuBorderColorValue = value
}
var readMenuBorderColorNight: Int
get() = readMenuBorderColorNightValue
set(value) {
readMenuBorderColorNightValue = value
}
val resolvedMenuBorderColor: Int val resolvedMenuBorderColor: Int
get() = if (ReadStyleResolver.isNightTheme()) readMenuBorderColorNight else readMenuBorderColor get() = if (ReadStyleResolver.isNightTheme()) readMenuBorderColorNight else readMenuBorderColor
var readMenuCustomIcons: Map<String, String>
get() = readMenuCustomIconsValue
set(value) {
readMenuCustomIconsValue = value.filterValues { it.isNotBlank() }
}
var titleBarCustomIcons: Map<String, String>
get() = titleBarCustomIconsValue
set(value) {
titleBarCustomIconsValue = value.filterValues { it.isNotBlank() }
}
// 0=top-left, 1=top-right, 2=bottom-left, 3=bottom-right
var titleBarIconPosition: Int
get() = titleBarIconPositionValue.coerceIn(0, 3)
set(value) {
titleBarIconPositionValue = value.coerceIn(0, 3)
}
var showTitleBarIcons: Boolean
get() = showTitleBarIconsValue
set(value) {
showTitleBarIconsValue = value
}
var readSliderMode: String
get() = readSliderModeValue
set(value) {
readSliderModeValue = value
}
fun encodeReadMenuCustomIcons(value: Map<String, String>): String {
return GSON.toJson(value.filterValues { it.isNotBlank() })
}
private fun parseReadMenuCustomIcons(value: String?): Map<String, String> {
if (value.isNullOrBlank()) {
return emptyMap()
}
return GSON.fromJsonObject<Map<String, String>>(value).getOrNull()
?.filterValues { it.isNotBlank() }
?: emptyMap()
}
val regexColorRules: ArrayList<RegexColorRule> get() = durConfig.regexColorRules val regexColorRules: ArrayList<RegexColorRule> get() = durConfig.regexColorRules
fun saveRegexColorRules() { fun saveRegexColorRules() {
save() save()
} }
/**
* 两端对齐
*/
val textFullJustify get() = textFullJustifyValue
/**
* 底部对齐
*/
val textBottomJustify get() = textBottomJustifyValue
var hideStatusBar: Boolean
get() = hideStatusBarValue
set(value) {
hideStatusBarValue = value
}
var hideNavigationBar: Boolean
get() = hideNavigationBarValue
set(value) {
hideNavigationBarValue = value
}
var useZhLayout: Boolean
get() = useZhLayoutValue
set(value) {
useZhLayoutValue = value
}
val config get() = if (shareLayout) shareConfig else durConfig val config get() = if (shareLayout) shareConfig else durConfig
var bgAlpha: Int var bgAlpha: Int
@@ -31,6 +31,7 @@ import io.legado.app.constant.NotificationId
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.readConfig.ReadConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.exoplayer.ExoPlayerHelper import io.legado.app.help.exoplayer.ExoPlayerHelper
import io.legado.app.help.glide.ImageLoader import io.legado.app.help.glide.ImageLoader
@@ -469,7 +470,7 @@ class AudioPlayService : BaseService(),
@SuppressLint("UnspecifiedImmutableFlag") @SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() { private fun initMediaSession() {
mediaSessionCompat = MediaSessionCompat(this, "readAloud") mediaSessionCompat = MediaSessionCompat(this, "readAloud")
if (AppConfig.systemMediaControlCompatibilityChange) { if (ReadConfig.systemMediaControlCompatibilityChange) {
mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() { mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
override fun onSeekTo(pos: Long) { override fun onSeekTo(pos: Long) {
position = pos.toInt() position = pos.toInt()
@@ -534,7 +535,7 @@ class AudioPlayService : BaseService(),
* 音频焦点变化 * 音频焦点变化
*/ */
override fun onAudioFocusChange(focusChange: Int) { override fun onAudioFocusChange(focusChange: Int) {
if (AppConfig.ignoreAudioFocus) { if (ReadConfig.ignoreAudioFocus) {
AppLog.put("忽略音频焦点处理(有声)") AppLog.put("忽略音频焦点处理(有声)")
return return
} }
@@ -630,7 +631,7 @@ class AudioPlayService : BaseService(),
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle { private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
mediaStyle.setShowActionsInCompactView(1,2,4) mediaStyle.setShowActionsInCompactView(1,2,4)
if (AppConfig.systemMediaControlCompatibilityChange) { if (ReadConfig.systemMediaControlCompatibilityChange) {
//fix #4090 android 14 can not show play control in lock screen //fix #4090 android 14 can not show play control in lock screen
mediaStyle.setMediaSession(mediaSessionCompat?.sessionToken) mediaStyle.setMediaSession(mediaSessionCompat?.sessionToken)
} }
@@ -669,7 +670,7 @@ class AudioPlayService : BaseService(),
* @return 音频焦点 * @return 音频焦点
*/ */
private fun requestFocus(): Boolean { private fun requestFocus(): Boolean {
if (AppConfig.ignoreAudioFocus) { if (ReadConfig.ignoreAudioFocus) {
return true return true
} }
return MediaHelp.requestFocus(mFocusRequest) return MediaHelp.requestFocus(mFocusRequest)
@@ -36,6 +36,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.readConfig.ReadConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.glide.ImageLoader import io.legado.app.help.glide.ImageLoader
import io.legado.app.lib.permission.Permissions import io.legado.app.lib.permission.Permissions
@@ -92,7 +93,7 @@ abstract class BaseReadAloudService : BaseService(),
} }
private val useWakeLock = appCtx.getPrefBoolean(PreferKey.readAloudWakeLock, false) private val useWakeLock = ReadConfig.readAloudWakeLock
private val wakeLock by lazy { private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:ReadAloudService") powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:ReadAloudService")
.apply { .apply {
@@ -151,9 +152,9 @@ abstract class BaseReadAloudService : BaseService(),
initBroadcastReceiver() initBroadcastReceiver()
initPhoneStateListener() initPhoneStateListener()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
setTimer(AppConfig.ttsTimer) setTimer(ReadConfig.ttsTimer)
if (AppConfig.ttsTimer > 0) { if (ReadConfig.ttsTimer > 0) {
toastOnUi("朗读定时 ${AppConfig.ttsTimer} 分钟") toastOnUi("朗读定时 ${ReadConfig.ttsTimer} 分钟")
} }
execute { execute {
ImageLoader ImageLoader
@@ -238,7 +239,7 @@ abstract class BaseReadAloudService : BaseService(),
return@execute return@execute
} }
readAloudNumber = textChapter.getReadLength(pageIndex) + startPos readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage) readAloudByPage = ReadConfig.readAloudByPage
contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0) contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0)
.split("\n") .split("\n")
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
@@ -429,7 +430,7 @@ abstract class BaseReadAloudService : BaseService(),
* @return 音频焦点 * @return 音频焦点
*/ */
fun requestFocus(): Boolean { fun requestFocus(): Boolean {
if (AppConfig.ignoreAudioFocus) { if (ReadConfig.ignoreAudioFocus) {
return true return true
} }
val requestFocus = MediaHelp.requestFocus(mFocusRequest) val requestFocus = MediaHelp.requestFocus(mFocusRequest)
@@ -495,7 +496,7 @@ abstract class BaseReadAloudService : BaseService(),
*/ */
@SuppressLint("UnspecifiedImmutableFlag") @SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() { private fun initMediaSession() {
if (AppConfig.systemMediaControlCompatibilityChange) { if (ReadConfig.systemMediaControlCompatibilityChange) {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() { override fun onPlay() {
resumeReadAloud() resumeReadAloud()
@@ -506,7 +507,7 @@ abstract class BaseReadAloudService : BaseService(),
} }
override fun onSkipToNext() { override fun onSkipToNext() {
if (getPrefBoolean("mediaButtonPerNext", false)) { if (ReadConfig.mediaButtonPerNext) {
nextChapter() nextChapter()
} else { } else {
nextP() nextP()
@@ -514,7 +515,7 @@ abstract class BaseReadAloudService : BaseService(),
} }
override fun onSkipToPrevious() { override fun onSkipToPrevious() {
if (getPrefBoolean("mediaButtonPerNext", false)) { if (ReadConfig.mediaButtonPerNext) {
prevChapter() prevChapter()
} else { } else {
prevP() prevP()
@@ -558,7 +559,7 @@ abstract class BaseReadAloudService : BaseService(),
* 音频焦点变化 * 音频焦点变化
*/ */
override fun onAudioFocusChange(focusChange: Int) { override fun onAudioFocusChange(focusChange: Int) {
if (AppConfig.ignoreAudioFocus) { if (ReadConfig.ignoreAudioFocus) {
AppLog.put("忽略音频焦点处理(TTS)") AppLog.put("忽略音频焦点处理(TTS)")
return return
} }
@@ -606,7 +607,7 @@ abstract class BaseReadAloudService : BaseService(),
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle { private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(1, 2, 4) .setShowActionsInCompactView(1, 2, 4)
if (AppConfig.systemMediaControlCompatibilityChange) { if (ReadConfig.systemMediaControlCompatibilityChange) {
//fix #4090 android 14 can not show play control in lock screen //fix #4090 android 14 can not show play control in lock screen
mediaStyle.setMediaSession(mediaSessionCompat.sessionToken) mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
} }
@@ -720,7 +721,7 @@ abstract class BaseReadAloudService : BaseService(),
} }
private fun initPhoneStateListener() { private fun initPhoneStateListener() {
val needRegister = AppConfig.ignoreAudioFocus && AppConfig.pauseReadAloudWhilePhoneCalls val needRegister = ReadConfig.ignoreAudioFocus && ReadConfig.pauseReadAloudWhilePhoneCalls
if (needRegister && registeredPhoneStateListener) { if (needRegister && registeredPhoneStateListener) {
return return
} }
@@ -35,6 +35,7 @@ import io.legado.app.data.entities.HttpTTS
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.readConfig.ReadConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.exoplayer.InputStreamDataSource import io.legado.app.help.exoplayer.InputStreamDataSource
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
@@ -128,7 +129,7 @@ class HttpReadAloudService : BaseReadAloudService(),
ReadBook.readAloud() ReadBook.readAloud()
} else { } else {
super.play() super.play()
if (AppConfig.streamReadAloudAudio) { if (ReadConfig.streamReadAloudAudio) {
downloadAndPlayAudiosStream() downloadAndPlayAudiosStream()
} else { } else {
downloadAndPlayAudios() downloadAndPlayAudios()
@@ -572,7 +573,7 @@ class HttpReadAloudService : BaseReadAloudService(),
downloadTask?.cancel() downloadTask?.cancel()
exoPlayer.stop() exoPlayer.stop()
speechRate = AppConfig.speechRatePlay + 5 speechRate = AppConfig.speechRatePlay + 5
if (AppConfig.streamReadAloudAudio) { if (ReadConfig.streamReadAloudAudio) {
downloadAndPlayAudiosStream() downloadAndPlayAudiosStream()
} else { } else {
downloadAndPlayAudios() downloadAndPlayAudios()
@@ -650,7 +651,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun deleteCurrentSpeakFile() { private fun deleteCurrentSpeakFile() {
if (AppConfig.streamReadAloudAudio) { if (ReadConfig.streamReadAloudAudio) {
return return
} }
val mediaItem = exoPlayer.currentMediaItem ?: return val mediaItem = exoPlayer.currentMediaItem ?: return
@@ -10,6 +10,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.readConfig.ReadConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.model.ReadAloud import io.legado.app.model.ReadAloud
@@ -152,13 +153,13 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
* 更新朗读速度 * 更新朗读速度
*/ */
override fun upSpeechRate(reset: Boolean) { override fun upSpeechRate(reset: Boolean) {
if (AppConfig.ttsFlowSys) { if (ReadConfig.ttsFollowSys) {
if (reset) { if (reset) {
clearTTS() clearTTS()
initTts() initTts()
} }
} else { } else {
val speechRate = (AppConfig.ttsSpeechRate + 5) / 10f val speechRate = (ReadConfig.ttsSpeechRate + 5) / 10f
textToSpeech?.setSpeechRate(speechRate) textToSpeech?.setSpeechRate(speechRate)
} }
} }
@@ -9,6 +9,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress import io.legado.app.data.entities.BookProgress
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.HttpTTS
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.searchContent.SearchResult import io.legado.app.ui.book.searchContent.SearchResult
@@ -134,6 +135,7 @@ data class ReadBookUiState(
val ttsEngineItems: ImmutableList<ReadBookTtsEngineItem> = persistentListOf(), val ttsEngineItems: ImmutableList<ReadBookTtsEngineItem> = persistentListOf(),
val selectedTtsEngine: String? = null, val selectedTtsEngine: String? = null,
val speakEngineName: String = "", val speakEngineName: String = "",
val editingHttpTts: HttpTTS? = null,
val preDownloadNum: Int = 10, val preDownloadNum: Int = 10,
val audioCacheCleanTime: Int = 10, val audioCacheCleanTime: Int = 10,
// Read aloud config // Read aloud config
@@ -416,6 +418,12 @@ sealed interface ReadBookIntent {
data class ApplySpeakEngine(val value: String?) : ReadBookIntent data class ApplySpeakEngine(val value: String?) : ReadBookIntent
data class ApplyPreDownloadNum(val value: Int) : ReadBookIntent data class ApplyPreDownloadNum(val value: Int) : ReadBookIntent
data class ApplyAudioCacheCleanTime(val value: Int) : ReadBookIntent data class ApplyAudioCacheCleanTime(val value: Int) : ReadBookIntent
data class EditHttpTts(val engineId: Long? = null) : ReadBookIntent
data class DeleteHttpTts(val engineId: Long) : ReadBookIntent
data class SaveHttpTts(val httpTTS: HttpTTS) : ReadBookIntent
data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent
data class ImportHttpTtsJson(val json: String) : ReadBookIntent
data object ExportAllHttpTts : ReadBookIntent
data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent
data class SetReadAloudPauseOnPhoneCall(val value: Boolean) : ReadBookIntent data class SetReadAloudPauseOnPhoneCall(val value: Boolean) : ReadBookIntent
data class SetReadAloudWakeLock(val value: Boolean) : ReadBookIntent data class SetReadAloudWakeLock(val value: Boolean) : ReadBookIntent
@@ -567,6 +575,9 @@ sealed interface ReadBookEffect {
data object UnregisterNetworkListener : ReadBookEffect data object UnregisterNetworkListener : ReadBookEffect
data object OpenBooksDirPicker : ReadBookEffect data object OpenBooksDirPicker : ReadBookEffect
data object BackupNow : ReadBookEffect data object BackupNow : ReadBookEffect
// Export — Activity handles file writing
data class ExportJson(val json: String) : ReadBookEffect
} }
@Immutable @Immutable
@@ -590,6 +601,7 @@ sealed interface ReadBookSheet {
data object BgTextConfig : ReadBookSheet data object BgTextConfig : ReadBookSheet
data object ReadAloudConfig : ReadBookSheet data object ReadAloudConfig : ReadBookSheet
data object SpeakEngineConfig : ReadBookSheet data object SpeakEngineConfig : ReadBookSheet
data class HttpTtsEdit(val engineId: Long? = null) : ReadBookSheet
data object PreDownloadConfig : ReadBookSheet data object PreDownloadConfig : ReadBookSheet
data object AudioCacheCleanConfig : ReadBookSheet data object AudioCacheCleanConfig : ReadBookSheet
data object ClickActionConfig : ReadBookSheet data object ClickActionConfig : ReadBookSheet
@@ -767,6 +767,7 @@ class ReadBookController(
is ReadBookEffect.OpenTitleBarCustomIconPicker, is ReadBookEffect.OpenTitleBarCustomIconPicker,
is ReadBookEffect.OpenSystemTtsSettings, is ReadBookEffect.OpenSystemTtsSettings,
is ReadBookEffect.TtsCacheCleared, is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson,
// DB query + bookmark effects — handled by ViewModel, ignored here // DB query + bookmark effects — handled by ViewModel, ignored here
is ReadBookEffect.MenuChangeSource, is ReadBookEffect.MenuChangeSource,
is ReadBookEffect.MenuBookChangeSource, is ReadBookEffect.MenuBookChangeSource,
@@ -30,6 +30,7 @@ import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
import io.legado.app.ui.book.read.sheet.ShadowSetSheet import io.legado.app.ui.book.read.sheet.ShadowSetSheet
import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet
import io.legado.app.ui.book.read.sheet.HttpTtsEditSheet
import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
import io.legado.app.ui.book.read.sheet.TitleBarIconSheet import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
@@ -198,13 +199,20 @@ fun ReadBookScreen(
) )
SpeakEngineConfigSheet( SpeakEngineConfigSheet(
show = state.activeSheet is ReadBookSheet.SpeakEngineConfig, show = state.activeSheet is ReadBookSheet.SpeakEngineConfig,
items = state.ttsEngineItems, state = state,
selectedValue = state.selectedTtsEngine, onIntent = onIntent,
onSelect = { onIntent(ReadBookIntent.ApplySpeakEngine(it)) },
onDismissRequest = { onDismissRequest = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig)) onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
}, },
) )
HttpTtsEditSheet(
show = state.activeSheet is ReadBookSheet.HttpTtsEdit,
httpTTS = state.editingHttpTts,
onIntent = onIntent,
onDismissRequest = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SpeakEngineConfig))
},
)
ReadAloudNumberConfigSheet( ReadAloudNumberConfigSheet(
show = state.activeSheet is ReadBookSheet.PreDownloadConfig, show = state.activeSheet is ReadBookSheet.PreDownloadConfig,
title = stringResource(R.string.read_aloud_preload), title = stringResource(R.string.read_aloud_preload),
@@ -20,6 +20,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress import io.legado.app.data.entities.BookProgress
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.data.entities.HttpTTS
import io.legado.app.data.repository.ReadAloudSettingsRepository import io.legado.app.data.repository.ReadAloudSettingsRepository
import io.legado.app.data.repository.ReadBookStyleConfigRepository import io.legado.app.data.repository.ReadBookStyleConfigRepository
import io.legado.app.data.repository.ReadPreferences import io.legado.app.data.repository.ReadPreferences
@@ -590,6 +591,90 @@ class ReadBookViewModel(
) )
} }
} }
is ReadBookIntent.EditHttpTts -> {
if (intent.engineId == null) {
_uiState.update {
it.copy(
editingHttpTts = HttpTTS(),
activeSheet = ReadBookSheet.HttpTtsEdit(),
)
}
} else {
execute {
appDb.httpTTSDao.get(intent.engineId)
}.onSuccess { tts ->
_uiState.update {
it.copy(
editingHttpTts = tts,
activeSheet = ReadBookSheet.HttpTtsEdit(intent.engineId),
)
}
}
}
}
is ReadBookIntent.DeleteHttpTts -> {
execute {
appDb.httpTTSDao.get(intent.engineId)?.let { tts ->
appDb.httpTTSDao.delete(tts)
}
}.onSuccess {
loadTtsEngineItems()
if (ReadAloud.ttsEngine == intent.engineId.toString()) {
AppConfig.ttsEngine = null
ReadAloud.upReadAloudClass()
}
}
}
is ReadBookIntent.SaveHttpTts -> {
execute {
appDb.httpTTSDao.insert(intent.httpTTS)
}.onSuccess {
loadTtsEngineItems()
_uiState.update {
it.copy(
editingHttpTts = null,
activeSheet = ReadBookSheet.SpeakEngineConfig,
)
}
}
}
is ReadBookIntent.ApplySpeakEnginePerBook -> {
ReadBook.book?.setTtsEngine(intent.value)
_uiState.update {
it.copy(
selectedTtsEngine = ReadAloud.ttsEngine,
speakEngineName = computeSpeakEngineName(),
activeSheet = ReadBookSheet.ReadAloudConfig,
)
}
}
is ReadBookIntent.ImportHttpTtsJson -> {
execute {
HttpTTS.fromJsonArray(intent.json).getOrDefault(arrayListOf())
}.onSuccess { list ->
if (list.isNotEmpty()) {
execute {
appDb.httpTTSDao.insert(*list.toTypedArray())
}.onSuccess {
loadTtsEngineItems()
}
}
}
}
is ReadBookIntent.ExportAllHttpTts -> {
execute {
GSON.toJson(appDb.httpTTSDao.all)
}.onSuccess { json ->
_effects.tryEmit(ReadBookEffect.ExportJson(json))
}
}
is ReadBookIntent.SetReadAloudIgnoreAudioFocus -> { is ReadBookIntent.SetReadAloudIgnoreAudioFocus -> {
viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) } viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) }
} }
@@ -1191,7 +1276,6 @@ class ReadBookViewModel(
readSettingsRepository.preferences.collect { preferences -> readSettingsRepository.preferences.collect { preferences ->
val old = previous val old = previous
previous = preferences previous = preferences
ReadBookConfig.syncPreferences(preferences)
AppConfig.syncReadPreferences(preferences) AppConfig.syncReadPreferences(preferences)
_readPreferences.value = preferences _readPreferences.value = preferences
if (!preferences.hasMenuClickArea()) { if (!preferences.hasMenuClickArea()) {
@@ -1,24 +1,56 @@
package io.legado.app.ui.book.read.sheet package io.legado.app.ui.book.read.sheet
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.HttpTTS
import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.book.read.ReadBookIntent
import io.legado.app.ui.book.read.ReadBookTtsEngineItem import io.legado.app.ui.book.read.ReadBookTtsEngineItem
import io.legado.app.ui.book.read.ReadBookUiState import io.legado.app.ui.book.read.ReadBookUiState
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
import io.legado.app.utils.GSON
@Composable @Composable
fun ReadAloudConfigSheet( fun ReadAloudConfigSheet(
@@ -125,33 +157,275 @@ fun ReadAloudConfigSheet(
@Composable @Composable
fun SpeakEngineConfigSheet( fun SpeakEngineConfigSheet(
show: Boolean, show: Boolean,
items: List<ReadBookTtsEngineItem>, state: ReadBookUiState,
selectedValue: String?, onIntent: (ReadBookIntent) -> Unit,
onSelect: (String?) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val clipboardManager = LocalClipboardManager.current
val items = state.ttsEngineItems
val selectedValue = state.selectedTtsEngine
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.speak_engine), title = stringResource(R.string.speak_engine),
startAction = {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
icon = Icons.Default.Add
)
//TODO: 导入
SmallTonalButton(
onClick = {
clipboardManager.getText()?.text?.let { text ->
if (text.isNotBlank()) {
onIntent(ReadBookIntent.ImportHttpTtsJson(text))
}
}
},
icon = Icons.Default.Description
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ExportAllHttpTts) },
icon = Icons.Default.FileDownload
)
}
},
endAction = {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ApplySpeakEnginePerBook(selectedValue)) },
text = stringResource(R.string.book)
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ApplySpeakEngine(selectedValue)) },
text = stringResource(R.string.general)
)
}
}
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .weight(1f, fill = false)
.padding(bottom = 8.dp),
) { ) {
items(items) { item -> items(items) { item ->
val isHttpTts = item.value != null
val isSelected = item.value == selectedValue
TinyClickableSettingItem( TinyClickableSettingItem(
title = item.title, title = item.title,
description = if (item.value == selectedValue) { description = if (isSelected) {
stringResource(R.string.default_version) stringResource(R.string.default_version)
} else { } else {
null null
}, },
onClick = { onSelect(item.value) }, onClick = { onIntent(ReadBookIntent.ApplySpeakEngine(item.value)) },
trailingContent = if (isHttpTts) {
{
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts(item.value!!.toLong())) },
icon = Icons.Default.Edit,
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.DeleteHttpTts(item.value!!.toLong())) },
icon = Icons.Default.Delete,
)
}
}
} else null,
) )
} }
} }
}
}
@Composable
fun HttpTtsEditSheet(
show: Boolean,
httpTTS: HttpTTS?,
onIntent: (ReadBookIntent) -> Unit,
onDismissRequest: () -> Unit,
) {
val tts = httpTTS ?: return
val clipboardManager = LocalClipboardManager.current
var name by remember(httpTTS) { mutableStateOf(tts.name) }
var url by remember(httpTTS) { mutableStateOf(tts.url) }
var contentType by remember(httpTTS) { mutableStateOf(tts.contentType ?: "") }
var concurrentRate by remember(httpTTS) { mutableStateOf(tts.concurrentRate ?: "0") }
var header by remember(httpTTS) { mutableStateOf(tts.header ?: "") }
var loginUrl by remember(httpTTS) { mutableStateOf(tts.loginUrl ?: "") }
var loginUi by remember(httpTTS) { mutableStateOf(tts.loginUi ?: "") }
var loginCheckJs by remember(httpTTS) { mutableStateOf(tts.loginCheckJs ?: "") }
var jsLib by remember(httpTTS) { mutableStateOf(tts.jsLib ?: "") }
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = stringResource(R.string.speak_engine),
startAction = {
var expanded by remember { mutableStateOf(false) }
Box {
SmallTonalButton(
onClick = { expanded = true },
icon = Icons.Default.MoreVert
)
RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
RoundDropdownMenuItem(
text = stringResource(R.string.copy_text),
onClick = {
expanded = false
val json = GSON.toJson(
tts.copy(
name = name, url = url, contentType = contentType,
concurrentRate = concurrentRate, header = header,
loginUrl = loginUrl, loginUi = loginUi,
loginCheckJs = loginCheckJs, jsLib = jsLib,
)
)
clipboardManager.setText(AnnotatedString(json))
},
)
RoundDropdownMenuItem(
text = stringResource(R.string.paste_source),
onClick = {
expanded = false
clipboardManager.getText()?.text?.let { text ->
HttpTTS.fromJson(text).getOrNull()?.let { imported ->
name = imported.name
url = imported.url
contentType = imported.contentType ?: ""
concurrentRate = imported.concurrentRate ?: "0"
header = imported.header ?: ""
loginUrl = imported.loginUrl ?: ""
loginUi = imported.loginUi ?: ""
loginCheckJs = imported.loginCheckJs ?: ""
jsLib = imported.jsLib ?: ""
}
}
},
)
}
}
},
endAction = {
SmallTonalButton(
icon = Icons.Default.Save,
onClick = {
onIntent(
ReadBookIntent.SaveHttpTts(
tts.copy(
name = name, url = url,
contentType = contentType.ifBlank { null },
concurrentRate = concurrentRate,
header = header.ifBlank { null },
loginUrl = loginUrl.ifBlank { null },
loginUi = loginUi.ifBlank { null },
loginCheckJs = loginCheckJs.ifBlank { null },
jsLib = jsLib.ifBlank { null },
)
)
)
},
)
},
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item {
AppTextField(
value = name,
onValueChange = { name = it },
label = stringResource(R.string.name),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = url,
onValueChange = { url = it },
label = "URL",
minLines = 2,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = contentType,
onValueChange = { contentType = it },
label = "Content-Type",
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = concurrentRate,
onValueChange = { concurrentRate = it },
label = stringResource(R.string.concurrent_rate),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = header,
onValueChange = { header = it },
label = stringResource(R.string.source_http_header),
minLines = 2,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = loginUrl,
onValueChange = { loginUrl = it },
label = stringResource(R.string.login_url),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = loginUi,
onValueChange = { loginUi = it },
label = stringResource(R.string.login_ui),
minLines = 2,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = loginCheckJs,
onValueChange = { loginCheckJs = it },
label = stringResource(R.string.login_check_js),
minLines = 2,
modifier = Modifier.fillMaxWidth(),
)
}
item {
AppTextField(
value = jsLib,
onValueChange = { jsLib = it },
label = "jsLib",
minLines = 2,
modifier = Modifier.fillMaxWidth(),
)
}
item { Spacer(Modifier.height(16.dp)) }
}
} }
} }
@@ -7,23 +7,27 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilledTonalButton import androidx.compose.material.icons.Icons
import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material3.Icon import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.book.read.ReadBookIntent
import io.legado.app.ui.book.read.ReadBookUiState import io.legado.app.ui.book.read.ReadBookUiState
import io.legado.app.ui.widget.components.button.series.MediumTonalButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
@@ -77,55 +81,34 @@ fun ReadAloudContent(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
FilledTonalIconButton( MediumTonalButton(
onClick = { onIntent(ReadBookIntent.ReadAloudPrevParagraph) }, onClick = { onIntent(ReadBookIntent.ReadAloudPrevParagraph) },
) { icon = Icons.Default.SkipPrevious,
Icon( contentDescription = stringResource(R.string.prev_sentence),
painter = painterResource(R.drawable.ic_skip_previous), )
contentDescription = stringResource(R.string.prev_sentence),
)
}
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
FilledTonalIconButton( MediumTonalButton(
onClick = { onClick = { onIntent(ReadBookIntent.ReadAloudTogglePause) },
onIntent(ReadBookIntent.ReadAloudTogglePause) icon = if (state.isReadAloudPaused) Icons.Default.PlayArrow else Icons.Default.Pause,
}, contentDescription = stringResource(
modifier = Modifier.size(48.dp), if (state.isReadAloudPaused) R.string.audio_play else R.string.pause
) { ),
Icon( )
painter = painterResource(
if (state.isReadAloudPaused) R.drawable.ic_play else R.drawable.ic_pause
),
contentDescription = stringResource(
if (state.isReadAloudPaused) R.string.audio_play else R.string.pause
),
)
}
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
FilledTonalIconButton( MediumTonalButton(
onClick = { onClick = {
onIntent(ReadBookIntent.ReadAloudStop) onIntent(ReadBookIntent.ReadAloudStop)
onDismissRequest() onDismissRequest()
}, },
modifier = Modifier.size(48.dp), icon = Icons.Default.Stop,
) { contentDescription = stringResource(R.string.stop),
Icon( )
painter = painterResource(R.drawable.ic_stop_black_24dp),
contentDescription = stringResource(R.string.stop),
)
}
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
FilledTonalButton( MediumTonalButton(
onClick = { onIntent(ReadBookIntent.ReadAloudNextParagraph) }, onClick = { onIntent(ReadBookIntent.ReadAloudNextParagraph) },
) { icon = Icons.Default.SkipNext,
Icon( text = stringResource(R.string.next_sentence),
painter = painterResource(R.drawable.ic_skip_next), )
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.next_sentence))
}
} }
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
@@ -147,26 +130,21 @@ fun ReadAloudContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
OutlinedButton( MediumTonalButton(
onClick = { onIntent(ReadBookIntent.ReadAloudPrevChapter) }, onClick = { onIntent(ReadBookIntent.ReadAloudPrevChapter) },
text = stringResource(R.string.previous_chapter),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { )
Text(stringResource(R.string.previous_chapter)) MediumTonalButton(
} onClick = { onIntent(ReadBookIntent.SetReadAloudTtsTimer(timerMinute)) },
FilledTonalButton( text = stringResource(R.string.timer_m, timerMinute),
onClick = {
onIntent(ReadBookIntent.SetReadAloudTtsTimer(timerMinute))
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { )
Text(stringResource(R.string.timer_m, timerMinute)) MediumTonalButton(
}
OutlinedButton(
onClick = { onIntent(ReadBookIntent.ReadAloudNextChapter) }, onClick = { onIntent(ReadBookIntent.ReadAloudNextChapter) },
text = stringResource(R.string.next_chapter),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { )
Text(stringResource(R.string.next_chapter))
}
} }
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
@@ -197,17 +175,17 @@ fun ReadAloudContent(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
ActionButton( ActionButton(
icon = R.drawable.ic_toc, icon = Icons.AutoMirrored.Filled.List,
label = stringResource(R.string.chapter_list), label = stringResource(R.string.chapter_list),
onClick = onOpenChapterList, onClick = onOpenChapterList,
) )
ActionButton( ActionButton(
icon = R.drawable.ic_visibility_off, icon = Icons.Default.VisibilityOff,
label = stringResource(R.string.to_backstage), label = stringResource(R.string.to_backstage),
onClick = onGoToBackground, onClick = onGoToBackground,
) )
ActionButton( ActionButton(
icon = R.drawable.ic_settings, icon = Icons.Default.Settings,
label = stringResource(R.string.setting), label = stringResource(R.string.setting),
onClick = onShowReadAloudConfig, onClick = onShowReadAloudConfig,
) )
@@ -217,19 +195,18 @@ fun ReadAloudContent(
@Composable @Composable
private fun ActionButton( private fun ActionButton(
icon: Int, icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String, label: String,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
FilledTonalIconButton(onClick = onClick) { MediumTonalButton(
Icon( onClick = onClick,
painter = painterResource(icon), icon = icon,
contentDescription = label, contentDescription = label,
) )
}
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = label, text = label,
@@ -189,4 +189,56 @@ object ReadConfig {
PreferKey.nextKeys, PreferKey.nextKeys,
"" ""
) )
// --- Read Aloud ---
var ignoreAudioFocus by prefDelegate(
PreferKey.ignoreAudioFocus,
false
)
var pauseReadAloudWhilePhoneCalls by prefDelegate(
PreferKey.pauseReadAloudWhilePhoneCalls,
false
)
var readAloudWakeLock by prefDelegate(
PreferKey.readAloudWakeLock,
false
)
var mediaButtonPerNext by prefDelegate(
"mediaButtonPerNext",
false
)
var readAloudByPage by prefDelegate(
PreferKey.readAloudByPage,
false
)
var systemMediaControlCompatibilityChange by prefDelegate(
PreferKey.systemMediaControlCompatibilityChange,
true
)
var streamReadAloudAudio by prefDelegate(
PreferKey.streamReadAloudAudio,
false
)
var ttsTimer by prefDelegate(
PreferKey.ttsTimer,
0
)
var ttsFollowSys by prefDelegate(
PreferKey.ttsFollowSys,
true
)
var ttsSpeechRate by prefDelegate(
PreferKey.ttsSpeechRate,
5
)
} }