fix: 朗读相关
This commit is contained in:
@@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.ui.config.readConfig.ReadConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -42,41 +43,53 @@ class ReadAloudSettingsRepository(
|
||||
preferences.toReadAloudPreferences()
|
||||
}
|
||||
|
||||
suspend fun setIgnoreAudioFocus(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.ignoreAudioFocus, value)
|
||||
suspend fun setIgnoreAudioFocus(value: Boolean) {
|
||||
ReadConfig.ignoreAudioFocus = value
|
||||
}
|
||||
|
||||
suspend fun setMediaButtonOnExit(value: Boolean) =
|
||||
suspend fun setMediaButtonOnExit(value: Boolean) {
|
||||
settingsRepository.putBoolean(PreferKey.mediaButtonOnExit, value)
|
||||
}
|
||||
|
||||
suspend fun setReadAloudByMediaButton(value: Boolean) =
|
||||
suspend fun setReadAloudByMediaButton(value: Boolean) {
|
||||
settingsRepository.putBoolean(PreferKey.readAloudByMediaButton, value)
|
||||
}
|
||||
|
||||
suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value)
|
||||
suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) {
|
||||
ReadConfig.pauseReadAloudWhilePhoneCalls = value
|
||||
}
|
||||
|
||||
suspend fun setReadAloudWakeLock(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.readAloudWakeLock, value)
|
||||
suspend fun setReadAloudWakeLock(value: Boolean) {
|
||||
ReadConfig.readAloudWakeLock = value
|
||||
}
|
||||
|
||||
suspend fun setMediaButtonPerNext(value: Boolean) =
|
||||
settingsRepository.putBoolean(KEY_MEDIA_BUTTON_PER_NEXT, value)
|
||||
suspend fun setMediaButtonPerNext(value: Boolean) {
|
||||
ReadConfig.mediaButtonPerNext = value
|
||||
}
|
||||
|
||||
suspend fun setReadAloudByPage(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.readAloudByPage, value)
|
||||
suspend fun setReadAloudByPage(value: Boolean) {
|
||||
ReadConfig.readAloudByPage = value
|
||||
}
|
||||
|
||||
suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.systemMediaControlCompatibilityChange, value)
|
||||
suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) {
|
||||
ReadConfig.systemMediaControlCompatibilityChange = value
|
||||
}
|
||||
|
||||
suspend fun setStreamReadAloudAudio(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.streamReadAloudAudio, value)
|
||||
suspend fun setStreamReadAloudAudio(value: Boolean) {
|
||||
ReadConfig.streamReadAloudAudio = value
|
||||
}
|
||||
|
||||
suspend fun setTtsTimer(value: Int) =
|
||||
settingsRepository.putInt(PreferKey.ttsTimer, value.coerceIn(0, 180))
|
||||
suspend fun setTtsTimer(value: Int) {
|
||||
ReadConfig.ttsTimer = value.coerceIn(0, 180)
|
||||
}
|
||||
|
||||
suspend fun setTtsFollowSys(value: Boolean) =
|
||||
settingsRepository.putBoolean(PreferKey.ttsFollowSys, value)
|
||||
suspend fun setTtsFollowSys(value: Boolean) {
|
||||
ReadConfig.ttsFollowSys = value
|
||||
}
|
||||
|
||||
suspend fun setTtsSpeechRate(value: Int) =
|
||||
settingsRepository.putInt(PreferKey.ttsSpeechRate, value.coerceIn(0, 80))
|
||||
suspend fun setTtsSpeechRate(value: Int) {
|
||||
ReadConfig.ttsSpeechRate = value.coerceIn(0, 80)
|
||||
}
|
||||
|
||||
private fun Preferences.toReadAloudPreferences(): ReadAloudPreferences {
|
||||
return ReadAloudPreferences(
|
||||
|
||||
@@ -376,9 +376,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var ttsFlowSys: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.ttsFollowSys, true)
|
||||
get() = ReadConfig.ttsFollowSys
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value)
|
||||
ReadConfig.ttsFollowSys = value
|
||||
}
|
||||
|
||||
val noAnimScrollPage: Boolean
|
||||
@@ -387,15 +387,15 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
const val defaultSpeechRate = 5
|
||||
|
||||
var ttsSpeechRate: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, defaultSpeechRate)
|
||||
get() = ReadConfig.ttsSpeechRate
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.ttsSpeechRate, value)
|
||||
ReadConfig.ttsSpeechRate = value
|
||||
}
|
||||
|
||||
var ttsTimer: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.ttsTimer, 0)
|
||||
get() = ReadConfig.ttsTimer
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.ttsTimer, value)
|
||||
ReadConfig.ttsTimer = value
|
||||
}
|
||||
|
||||
val speechRatePlay: Int get() = if (ttsFlowSys) defaultSpeechRate else ttsSpeechRate
|
||||
@@ -604,11 +604,17 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
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
|
||||
get() = appCtx.getPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, false)
|
||||
set(value) = appCtx.putPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value)
|
||||
get() = ReadConfig.pauseReadAloudWhilePhoneCalls
|
||||
set(value) {
|
||||
ReadConfig.pauseReadAloudWhilePhoneCalls = value
|
||||
}
|
||||
|
||||
val onlyLatestBackup get() = BackupConfig.onlyLatestBackup
|
||||
|
||||
@@ -618,7 +624,11 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
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?
|
||||
get() = ReadConfig.doubleHorizontalPage
|
||||
@@ -940,9 +950,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var systemMediaControlCompatibilityChange: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.systemMediaControlCompatibilityChange, true)
|
||||
get() = ReadConfig.systemMediaControlCompatibilityChange
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.systemMediaControlCompatibilityChange, value)
|
||||
ReadConfig.systemMediaControlCompatibilityChange = value
|
||||
}
|
||||
|
||||
var isPredictiveBackEnabled: Boolean
|
||||
|
||||
@@ -7,36 +7,25 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.Keep
|
||||
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.constant.PageAnim
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.ReadMenuBlurMode
|
||||
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.dataStore
|
||||
import io.legado.app.help.DefaultData
|
||||
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.fromJsonObject
|
||||
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.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 java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* 阅读界面配置
|
||||
@@ -46,6 +35,40 @@ import java.io.InputStream
|
||||
object ReadBookConfig {
|
||||
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
|
||||
const val tipNone = 0
|
||||
const val tipChapterTitle = 1
|
||||
@@ -94,23 +117,6 @@ object ReadBookConfig {
|
||||
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
|
||||
fun getConfig(index: Int): Config {
|
||||
if (configList.size < 5) {
|
||||
@@ -179,195 +185,115 @@ object ReadBookConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime compatibility snapshot. New write/read flows should use ReadSettingsRepository.
|
||||
private var readBodyToLhValue = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
|
||||
var readBodyToLh: Boolean
|
||||
get() = readBodyToLhValue
|
||||
set(value) {
|
||||
readBodyToLhValue = value
|
||||
}
|
||||
// region Scalar properties (prefDelegate — auto-syncs SP + DataStore)
|
||||
|
||||
private var autoReadSpeedValue = appCtx.getPrefInt(PreferKey.autoReadSpeed, 10)
|
||||
var autoReadSpeed: Int
|
||||
get() = autoReadSpeedValue
|
||||
set(value) {
|
||||
autoReadSpeedValue = value
|
||||
appCtx.putPrefInt(PreferKey.autoReadSpeed, value)
|
||||
}
|
||||
var readBodyToLh by prefDelegate(PreferKey.readBodyToLh, true)
|
||||
var autoReadSpeed by prefDelegate(PreferKey.autoReadSpeed, 10)
|
||||
var readStyleSelect by prefDelegate(PreferKey.readStyleSelect, 0)
|
||||
var comicStyleSelect by prefDelegate(PreferKey.comicStyleSelect, 0)
|
||||
var shareLayout by prefDelegate(PreferKey.shareLayout, false)
|
||||
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
|
||||
get() = if (isComic) comicStyleSelect else readStyleSelect
|
||||
set(value) {
|
||||
if (isComic) {
|
||||
comicStyleSelect = value
|
||||
} else {
|
||||
readStyleSelect = value
|
||||
}
|
||||
if (isComic) comicStyleSelect = value else readStyleSelect = value
|
||||
}
|
||||
|
||||
private var readStyleSelectValue = readIntFromDataStore(PreferKey.readStyleSelect, 0)
|
||||
var readStyleSelect: Int
|
||||
get() = readStyleSelectValue
|
||||
set(value) {
|
||||
readStyleSelectValue = value
|
||||
if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) {
|
||||
appCtx.putPrefInt(PreferKey.readStyleSelect, value)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
private var comicStyleSelectValue =
|
||||
readIntFromDataStore(PreferKey.comicStyleSelect, readStyleSelect)
|
||||
var comicStyleSelect: Int
|
||||
get() = comicStyleSelectValue
|
||||
set(value) {
|
||||
comicStyleSelectValue = value
|
||||
if (appCtx.getPrefInt(PreferKey.comicStyleSelect) != value) {
|
||||
appCtx.putPrefInt(PreferKey.comicStyleSelect, value)
|
||||
}
|
||||
}
|
||||
// region coerceIn properties (clampedPrefDelegate)
|
||||
|
||||
private var shareLayoutValue = appCtx.getPrefBoolean(PreferKey.shareLayout)
|
||||
var shareLayout: Boolean
|
||||
get() = shareLayoutValue
|
||||
set(value) {
|
||||
shareLayoutValue = value
|
||||
if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) {
|
||||
appCtx.putPrefBoolean(PreferKey.shareLayout, value)
|
||||
}
|
||||
}
|
||||
var readMenuColorMode by clampedPrefDelegate(PreferKey.readMenuColorMode, 1, 0..1)
|
||||
var readMenuIconStyle by clampedPrefDelegate(PreferKey.readMenuIconStyle, 0, 0..2)
|
||||
var readMenuIconItemsPerRow by clampedPrefDelegate(PreferKey.readMenuIconItemsPerRow, 5, 2..8)
|
||||
var readMenuIconRowCount by clampedPrefDelegate(PreferKey.readMenuIconRowCount, 1, 1..2)
|
||||
var readMenuBottomCornerRadius by clampedPrefDelegate(PreferKey.readMenuBottomCornerRadius, 0, 0..32)
|
||||
var readMenuTopBarBlurMode by clampedPrefDelegate(PreferKey.readMenuTopBarBlurMode, ReadMenuBlurMode.None, 0..2)
|
||||
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)
|
||||
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"
|
||||
// endregion
|
||||
|
||||
fun syncPreferences(preferences: ReadPreferences) {
|
||||
readBodyToLhValue = preferences.readBodyToLh
|
||||
autoReadSpeedValue = preferences.autoReadSpeed
|
||||
readStyleSelectValue = preferences.readStyleSelect
|
||||
comicStyleSelectValue = preferences.comicStyleSelect
|
||||
shareLayoutValue = preferences.shareLayout
|
||||
textFullJustifyValue = preferences.textFullJustify
|
||||
textBottomJustifyValue = preferences.textBottomJustify
|
||||
hideStatusBarValue = preferences.hideStatusBar
|
||||
hideNavigationBarValue = preferences.hideNavigationBar
|
||||
useZhLayoutValue = preferences.useZhLayout
|
||||
readMenuBgColorValue = preferences.readMenuBgColor
|
||||
readMenuAccentColorValue = preferences.readMenuAccentColor
|
||||
readMenuContainerColorValue = preferences.readMenuContainerColor
|
||||
readMenuBgColorNightValue = preferences.readMenuBgColorNight
|
||||
readMenuAccentColorNightValue = preferences.readMenuAccentColorNight
|
||||
readMenuContainerColorNightValue = preferences.readMenuContainerColorNight
|
||||
readMenuColorModeValue = preferences.readMenuColorMode
|
||||
readMenuIconShowTextValue = preferences.readMenuIconShowText
|
||||
readMenuIconStyleValue = preferences.readMenuIconStyle
|
||||
readMenuIconItemsPerRowValue = preferences.readMenuIconItemsPerRow
|
||||
readMenuIconRowCountValue = preferences.readMenuIconRowCount
|
||||
readMenuBottomCornerRadiusValue = preferences.readMenuBottomCornerRadius
|
||||
readMenuFloatingBottomBarValue = preferences.readMenuFloatingBottomBar
|
||||
readMenuTopBarBlurModeValue = preferences.readMenuTopBarBlurMode
|
||||
readMenuBottomBarBlurModeValue = preferences.readMenuBottomBarBlurMode
|
||||
readMenuTopBarLiquidGlassButtonsValue = preferences.readMenuTopBarLiquidGlassButtons
|
||||
readMenuBottomBarLiquidGlassButtonsValue = preferences.readMenuBottomBarLiquidGlassButtons
|
||||
readMenuTopBarBlurStyleValue = preferences.readMenuTopBarBlurStyle
|
||||
readMenuBottomBarBlurStyleValue = preferences.readMenuBottomBarBlurStyle
|
||||
readMenuBlurRadiusValue = preferences.readMenuBlurRadius
|
||||
readMenuBlurAlphaValue = preferences.readMenuBlurAlpha
|
||||
readMenuLensRadiusValue = preferences.readMenuLensRadius
|
||||
readMenuBorderWidthValue = preferences.readMenuBorderWidth
|
||||
readMenuBorderColorValue = preferences.readMenuBorderColor
|
||||
readMenuBorderColorNightValue = preferences.readMenuBorderColorNight
|
||||
readMenuCustomIconsValue = parseReadMenuCustomIcons(preferences.readMenuCustomIcons)
|
||||
titleBarCustomIconsValue = parseReadMenuCustomIcons(preferences.titleBarCustomIcons)
|
||||
titleBarIconPositionValue = preferences.titleBarIconPosition
|
||||
showTitleBarIconsValue = preferences.showTitleBarIcons
|
||||
readSliderModeValue = preferences.readSliderMode
|
||||
// region Color properties (prefDelegate backing + custom fallback getter)
|
||||
|
||||
private val readMenuBgColorDelegate = prefDelegate(PreferKey.readMenuBgColor, 0)
|
||||
var readMenuBgColor: Int
|
||||
get() = readMenuBgColorDelegate.getValue(this, ::readMenuBgColor)
|
||||
.takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = false)
|
||||
set(value) = readMenuBgColorDelegate.setValue(this, ::readMenuBgColor, value)
|
||||
|
||||
private val readMenuAccentColorDelegate = prefDelegate(PreferKey.readMenuAccentColor, 0)
|
||||
var readMenuAccentColor: Int
|
||||
get() = readMenuAccentColorDelegate.getValue(this, ::readMenuAccentColor)
|
||||
.takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = false)
|
||||
set(value) = readMenuAccentColorDelegate.setValue(this, ::readMenuAccentColor, value)
|
||||
|
||||
private val readMenuContainerColorDelegate = prefDelegate(PreferKey.readMenuContainerColor, 0)
|
||||
var readMenuContainerColor: Int
|
||||
get() = readMenuContainerColorDelegate.getValue(this, ::readMenuContainerColor)
|
||||
.takeIf { it != 0 } ?: readMenuBgColor
|
||||
set(value) = readMenuContainerColorDelegate.setValue(this, ::readMenuContainerColor, value)
|
||||
|
||||
private val readMenuBgColorNightDelegate = prefDelegate(PreferKey.readMenuBgColorNight, 0)
|
||||
var readMenuBgColorNight: Int
|
||||
get() = readMenuBgColorNightDelegate.getValue(this, ::readMenuBgColorNight)
|
||||
.takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = true)
|
||||
set(value) = readMenuBgColorNightDelegate.setValue(this, ::readMenuBgColorNight, value)
|
||||
|
||||
private val readMenuAccentColorNightDelegate = prefDelegate(PreferKey.readMenuAccentColorNight, 0)
|
||||
var readMenuAccentColorNight: Int
|
||||
get() = readMenuAccentColorNightDelegate.getValue(this, ::readMenuAccentColorNight)
|
||||
.takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = true)
|
||||
set(value) = readMenuAccentColorNightDelegate.setValue(this, ::readMenuAccentColorNight, value)
|
||||
|
||||
private val readMenuContainerColorNightDelegate = prefDelegate(PreferKey.readMenuContainerColorNight, 0)
|
||||
var readMenuContainerColorNight: Int
|
||||
get() = readMenuContainerColorNightDelegate.getValue(this, ::readMenuContainerColorNight)
|
||||
.takeIf { it != 0 } ?: readMenuBgColorNight
|
||||
set(value) = readMenuContainerColorNightDelegate.setValue(this, ::readMenuContainerColorNight, value)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Map properties (PrefDelegateMap — JSON string serialization)
|
||||
|
||||
fun encodeReadMenuCustomIcons(value: Map<String, String>): String {
|
||||
return GSON.toJson(value.filterValues { it.isNotBlank() })
|
||||
}
|
||||
|
||||
var readMenuBgColor: Int
|
||||
get() = readMenuBgColorValue.takeIf { it != 0 } ?: durConfig.menuBgColor(isNight = false)
|
||||
set(value) {
|
||||
readMenuBgColorValue = value
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
var readMenuAccentColor: Int
|
||||
get() = readMenuAccentColorValue.takeIf { it != 0 } ?: durConfig.menuAccentColor(isNight = false)
|
||||
set(value) {
|
||||
readMenuAccentColorValue = value
|
||||
}
|
||||
var readMenuCustomIcons: Map<String, String> by PrefDelegateMap(
|
||||
PreferKey.readMenuCustomIcons, ::parseReadMenuCustomIcons, ::encodeReadMenuCustomIcons,
|
||||
)
|
||||
|
||||
var readMenuContainerColor: Int
|
||||
get() = readMenuContainerColorValue.takeIf { it != 0 } ?: readMenuBgColor
|
||||
set(value) {
|
||||
readMenuContainerColorValue = value
|
||||
}
|
||||
var titleBarCustomIcons: Map<String, String> by PrefDelegateMap(
|
||||
PreferKey.titleBarCustomIcons, ::parseReadMenuCustomIcons, ::encodeReadMenuCustomIcons,
|
||||
)
|
||||
|
||||
var readMenuBgColorNight: Int
|
||||
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
|
||||
}
|
||||
// endregion
|
||||
|
||||
val resolvedMenuBgColor: Int
|
||||
get() {
|
||||
@@ -400,198 +326,15 @@ object ReadBookConfig {
|
||||
val resolvedMenuContainerColor: Int
|
||||
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
|
||||
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
|
||||
|
||||
fun saveRegexColorRules() {
|
||||
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
|
||||
|
||||
var bgAlpha: Int
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.legado.app.constant.NotificationId
|
||||
import io.legado.app.constant.Status
|
||||
import io.legado.app.help.MediaHelp
|
||||
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.exoplayer.ExoPlayerHelper
|
||||
import io.legado.app.help.glide.ImageLoader
|
||||
@@ -469,7 +470,7 @@ class AudioPlayService : BaseService(),
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private fun initMediaSession() {
|
||||
mediaSessionCompat = MediaSessionCompat(this, "readAloud")
|
||||
if (AppConfig.systemMediaControlCompatibilityChange) {
|
||||
if (ReadConfig.systemMediaControlCompatibilityChange) {
|
||||
mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
|
||||
override fun onSeekTo(pos: Long) {
|
||||
position = pos.toInt()
|
||||
@@ -534,7 +535,7 @@ class AudioPlayService : BaseService(),
|
||||
* 音频焦点变化
|
||||
*/
|
||||
override fun onAudioFocusChange(focusChange: Int) {
|
||||
if (AppConfig.ignoreAudioFocus) {
|
||||
if (ReadConfig.ignoreAudioFocus) {
|
||||
AppLog.put("忽略音频焦点处理(有声)")
|
||||
return
|
||||
}
|
||||
@@ -630,7 +631,7 @@ class AudioPlayService : BaseService(),
|
||||
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
|
||||
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
|
||||
mediaStyle.setShowActionsInCompactView(1,2,4)
|
||||
if (AppConfig.systemMediaControlCompatibilityChange) {
|
||||
if (ReadConfig.systemMediaControlCompatibilityChange) {
|
||||
//fix #4090 android 14 can not show play control in lock screen
|
||||
mediaStyle.setMediaSession(mediaSessionCompat?.sessionToken)
|
||||
}
|
||||
@@ -669,7 +670,7 @@ class AudioPlayService : BaseService(),
|
||||
* @return 音频焦点
|
||||
*/
|
||||
private fun requestFocus(): Boolean {
|
||||
if (AppConfig.ignoreAudioFocus) {
|
||||
if (ReadConfig.ignoreAudioFocus) {
|
||||
return true
|
||||
}
|
||||
return MediaHelp.requestFocus(mFocusRequest)
|
||||
|
||||
@@ -36,6 +36,7 @@ import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Status
|
||||
import io.legado.app.help.MediaHelp
|
||||
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.glide.ImageLoader
|
||||
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 {
|
||||
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:ReadAloudService")
|
||||
.apply {
|
||||
@@ -151,9 +152,9 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
initBroadcastReceiver()
|
||||
initPhoneStateListener()
|
||||
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
|
||||
setTimer(AppConfig.ttsTimer)
|
||||
if (AppConfig.ttsTimer > 0) {
|
||||
toastOnUi("朗读定时 ${AppConfig.ttsTimer} 分钟")
|
||||
setTimer(ReadConfig.ttsTimer)
|
||||
if (ReadConfig.ttsTimer > 0) {
|
||||
toastOnUi("朗读定时 ${ReadConfig.ttsTimer} 分钟")
|
||||
}
|
||||
execute {
|
||||
ImageLoader
|
||||
@@ -238,7 +239,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
return@execute
|
||||
}
|
||||
readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
|
||||
readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
|
||||
readAloudByPage = ReadConfig.readAloudByPage
|
||||
contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0)
|
||||
.split("\n")
|
||||
.filter { it.isNotEmpty() }
|
||||
@@ -429,7 +430,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
* @return 音频焦点
|
||||
*/
|
||||
fun requestFocus(): Boolean {
|
||||
if (AppConfig.ignoreAudioFocus) {
|
||||
if (ReadConfig.ignoreAudioFocus) {
|
||||
return true
|
||||
}
|
||||
val requestFocus = MediaHelp.requestFocus(mFocusRequest)
|
||||
@@ -495,7 +496,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
*/
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private fun initMediaSession() {
|
||||
if (AppConfig.systemMediaControlCompatibilityChange) {
|
||||
if (ReadConfig.systemMediaControlCompatibilityChange) {
|
||||
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
resumeReadAloud()
|
||||
@@ -506,7 +507,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
if (getPrefBoolean("mediaButtonPerNext", false)) {
|
||||
if (ReadConfig.mediaButtonPerNext) {
|
||||
nextChapter()
|
||||
} else {
|
||||
nextP()
|
||||
@@ -514,7 +515,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
if (getPrefBoolean("mediaButtonPerNext", false)) {
|
||||
if (ReadConfig.mediaButtonPerNext) {
|
||||
prevChapter()
|
||||
} else {
|
||||
prevP()
|
||||
@@ -558,7 +559,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
* 音频焦点变化
|
||||
*/
|
||||
override fun onAudioFocusChange(focusChange: Int) {
|
||||
if (AppConfig.ignoreAudioFocus) {
|
||||
if (ReadConfig.ignoreAudioFocus) {
|
||||
AppLog.put("忽略音频焦点处理(TTS)")
|
||||
return
|
||||
}
|
||||
@@ -606,7 +607,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
|
||||
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setShowActionsInCompactView(1, 2, 4)
|
||||
if (AppConfig.systemMediaControlCompatibilityChange) {
|
||||
if (ReadConfig.systemMediaControlCompatibilityChange) {
|
||||
//fix #4090 android 14 can not show play control in lock screen
|
||||
mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
|
||||
}
|
||||
@@ -720,7 +721,7 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
}
|
||||
|
||||
private fun initPhoneStateListener() {
|
||||
val needRegister = AppConfig.ignoreAudioFocus && AppConfig.pauseReadAloudWhilePhoneCalls
|
||||
val needRegister = ReadConfig.ignoreAudioFocus && ReadConfig.pauseReadAloudWhilePhoneCalls
|
||||
if (needRegister && registeredPhoneStateListener) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
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.exoplayer.InputStreamDataSource
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
@@ -128,7 +129,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
ReadBook.readAloud()
|
||||
} else {
|
||||
super.play()
|
||||
if (AppConfig.streamReadAloudAudio) {
|
||||
if (ReadConfig.streamReadAloudAudio) {
|
||||
downloadAndPlayAudiosStream()
|
||||
} else {
|
||||
downloadAndPlayAudios()
|
||||
@@ -572,7 +573,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
downloadTask?.cancel()
|
||||
exoPlayer.stop()
|
||||
speechRate = AppConfig.speechRatePlay + 5
|
||||
if (AppConfig.streamReadAloudAudio) {
|
||||
if (ReadConfig.streamReadAloudAudio) {
|
||||
downloadAndPlayAudiosStream()
|
||||
} else {
|
||||
downloadAndPlayAudios()
|
||||
@@ -650,7 +651,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
}
|
||||
|
||||
private fun deleteCurrentSpeakFile() {
|
||||
if (AppConfig.streamReadAloudAudio) {
|
||||
if (ReadConfig.streamReadAloudAudio) {
|
||||
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.help.MediaHelp
|
||||
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.lib.dialogs.SelectItem
|
||||
import io.legado.app.model.ReadAloud
|
||||
@@ -152,13 +153,13 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
|
||||
* 更新朗读速度
|
||||
*/
|
||||
override fun upSpeechRate(reset: Boolean) {
|
||||
if (AppConfig.ttsFlowSys) {
|
||||
if (ReadConfig.ttsFollowSys) {
|
||||
if (reset) {
|
||||
clearTTS()
|
||||
initTts()
|
||||
}
|
||||
} else {
|
||||
val speechRate = (AppConfig.ttsSpeechRate + 5) / 10f
|
||||
val speechRate = (ReadConfig.ttsSpeechRate + 5) / 10f
|
||||
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.BookProgress
|
||||
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.TextPage
|
||||
import io.legado.app.ui.book.searchContent.SearchResult
|
||||
@@ -134,6 +135,7 @@ data class ReadBookUiState(
|
||||
val ttsEngineItems: ImmutableList<ReadBookTtsEngineItem> = persistentListOf(),
|
||||
val selectedTtsEngine: String? = null,
|
||||
val speakEngineName: String = "",
|
||||
val editingHttpTts: HttpTTS? = null,
|
||||
val preDownloadNum: Int = 10,
|
||||
val audioCacheCleanTime: Int = 10,
|
||||
// Read aloud config
|
||||
@@ -416,6 +418,12 @@ sealed interface ReadBookIntent {
|
||||
data class ApplySpeakEngine(val value: String?) : ReadBookIntent
|
||||
data class ApplyPreDownloadNum(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 SetReadAloudPauseOnPhoneCall(val value: Boolean) : ReadBookIntent
|
||||
data class SetReadAloudWakeLock(val value: Boolean) : ReadBookIntent
|
||||
@@ -567,6 +575,9 @@ sealed interface ReadBookEffect {
|
||||
data object UnregisterNetworkListener : ReadBookEffect
|
||||
data object OpenBooksDirPicker : ReadBookEffect
|
||||
data object BackupNow : ReadBookEffect
|
||||
|
||||
// Export — Activity handles file writing
|
||||
data class ExportJson(val json: String) : ReadBookEffect
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -590,6 +601,7 @@ sealed interface ReadBookSheet {
|
||||
data object BgTextConfig : ReadBookSheet
|
||||
data object ReadAloudConfig : ReadBookSheet
|
||||
data object SpeakEngineConfig : ReadBookSheet
|
||||
data class HttpTtsEdit(val engineId: Long? = null) : ReadBookSheet
|
||||
data object PreDownloadConfig : ReadBookSheet
|
||||
data object AudioCacheCleanConfig : ReadBookSheet
|
||||
data object ClickActionConfig : ReadBookSheet
|
||||
|
||||
@@ -767,6 +767,7 @@ class ReadBookController(
|
||||
is ReadBookEffect.OpenTitleBarCustomIconPicker,
|
||||
is ReadBookEffect.OpenSystemTtsSettings,
|
||||
is ReadBookEffect.TtsCacheCleared,
|
||||
is ReadBookEffect.ExportJson,
|
||||
// DB query + bookmark effects — handled by ViewModel, ignored here
|
||||
is ReadBookEffect.MenuChangeSource,
|
||||
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.ShadowSetSheet
|
||||
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.TitleBarIconSheet
|
||||
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
|
||||
@@ -198,13 +199,20 @@ fun ReadBookScreen(
|
||||
)
|
||||
SpeakEngineConfigSheet(
|
||||
show = state.activeSheet is ReadBookSheet.SpeakEngineConfig,
|
||||
items = state.ttsEngineItems,
|
||||
selectedValue = state.selectedTtsEngine,
|
||||
onSelect = { onIntent(ReadBookIntent.ApplySpeakEngine(it)) },
|
||||
state = state,
|
||||
onIntent = onIntent,
|
||||
onDismissRequest = {
|
||||
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
|
||||
},
|
||||
)
|
||||
HttpTtsEditSheet(
|
||||
show = state.activeSheet is ReadBookSheet.HttpTtsEdit,
|
||||
httpTTS = state.editingHttpTts,
|
||||
onIntent = onIntent,
|
||||
onDismissRequest = {
|
||||
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SpeakEngineConfig))
|
||||
},
|
||||
)
|
||||
ReadAloudNumberConfigSheet(
|
||||
show = state.activeSheet is ReadBookSheet.PreDownloadConfig,
|
||||
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.BookProgress
|
||||
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.ReadBookStyleConfigRepository
|
||||
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 -> {
|
||||
viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) }
|
||||
}
|
||||
@@ -1191,7 +1276,6 @@ class ReadBookViewModel(
|
||||
readSettingsRepository.preferences.collect { preferences ->
|
||||
val old = previous
|
||||
previous = preferences
|
||||
ReadBookConfig.syncPreferences(preferences)
|
||||
AppConfig.syncReadPreferences(preferences)
|
||||
_readPreferences.value = preferences
|
||||
if (!preferences.hasMenuClickArea()) {
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.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.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ReadBookTtsEngineItem
|
||||
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.settingItem.SliderSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
|
||||
import io.legado.app.utils.GSON
|
||||
|
||||
@Composable
|
||||
fun ReadAloudConfigSheet(
|
||||
@@ -125,33 +157,275 @@ fun ReadAloudConfigSheet(
|
||||
@Composable
|
||||
fun SpeakEngineConfigSheet(
|
||||
show: Boolean,
|
||||
items: List<ReadBookTtsEngineItem>,
|
||||
selectedValue: String?,
|
||||
onSelect: (String?) -> Unit,
|
||||
state: ReadBookUiState,
|
||||
onIntent: (ReadBookIntent) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val items = state.ttsEngineItems
|
||||
val selectedValue = state.selectedTtsEngine
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
.weight(1f, fill = false)
|
||||
.padding(bottom = 8.dp),
|
||||
) {
|
||||
items(items) { item ->
|
||||
val isHttpTts = item.value != null
|
||||
val isSelected = item.value == selectedValue
|
||||
TinyClickableSettingItem(
|
||||
title = item.title,
|
||||
description = if (item.value == selectedValue) {
|
||||
description = if (isSelected) {
|
||||
stringResource(R.string.default_version)
|
||||
} else {
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
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.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.book.read.ReadBookIntent
|
||||
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.settingItem.TinySliderSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
|
||||
@@ -77,55 +81,34 @@ fun ReadAloudContent(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.ReadAloudPrevParagraph) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_skip_previous),
|
||||
contentDescription = stringResource(R.string.prev_sentence),
|
||||
)
|
||||
}
|
||||
icon = Icons.Default.SkipPrevious,
|
||||
contentDescription = stringResource(R.string.prev_sentence),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
FilledTonalIconButton(
|
||||
onClick = {
|
||||
onIntent(ReadBookIntent.ReadAloudTogglePause)
|
||||
},
|
||||
modifier = Modifier.size(48.dp),
|
||||
) {
|
||||
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
|
||||
),
|
||||
)
|
||||
}
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.ReadAloudTogglePause) },
|
||||
icon = if (state.isReadAloudPaused) Icons.Default.PlayArrow else Icons.Default.Pause,
|
||||
contentDescription = stringResource(
|
||||
if (state.isReadAloudPaused) R.string.audio_play else R.string.pause
|
||||
),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
FilledTonalIconButton(
|
||||
MediumTonalButton(
|
||||
onClick = {
|
||||
onIntent(ReadBookIntent.ReadAloudStop)
|
||||
onDismissRequest()
|
||||
},
|
||||
modifier = Modifier.size(48.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_stop_black_24dp),
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
)
|
||||
}
|
||||
icon = Icons.Default.Stop,
|
||||
contentDescription = stringResource(R.string.stop),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
FilledTonalButton(
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.ReadAloudNextParagraph) },
|
||||
) {
|
||||
Icon(
|
||||
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))
|
||||
}
|
||||
icon = Icons.Default.SkipNext,
|
||||
text = stringResource(R.string.next_sentence),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
@@ -147,26 +130,21 @@ fun ReadAloudContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
OutlinedButton(
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.ReadAloudPrevChapter) },
|
||||
text = stringResource(R.string.previous_chapter),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(stringResource(R.string.previous_chapter))
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
onIntent(ReadBookIntent.SetReadAloudTtsTimer(timerMinute))
|
||||
},
|
||||
)
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.SetReadAloudTtsTimer(timerMinute)) },
|
||||
text = stringResource(R.string.timer_m, timerMinute),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(stringResource(R.string.timer_m, timerMinute))
|
||||
}
|
||||
OutlinedButton(
|
||||
)
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.ReadAloudNextChapter) },
|
||||
text = stringResource(R.string.next_chapter),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(stringResource(R.string.next_chapter))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
@@ -197,17 +175,17 @@ fun ReadAloudContent(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
ActionButton(
|
||||
icon = R.drawable.ic_toc,
|
||||
icon = Icons.AutoMirrored.Filled.List,
|
||||
label = stringResource(R.string.chapter_list),
|
||||
onClick = onOpenChapterList,
|
||||
)
|
||||
ActionButton(
|
||||
icon = R.drawable.ic_visibility_off,
|
||||
icon = Icons.Default.VisibilityOff,
|
||||
label = stringResource(R.string.to_backstage),
|
||||
onClick = onGoToBackground,
|
||||
)
|
||||
ActionButton(
|
||||
icon = R.drawable.ic_settings,
|
||||
icon = Icons.Default.Settings,
|
||||
label = stringResource(R.string.setting),
|
||||
onClick = onShowReadAloudConfig,
|
||||
)
|
||||
@@ -217,19 +195,18 @@ fun ReadAloudContent(
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(
|
||||
icon: Int,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
FilledTonalIconButton(onClick = onClick) {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = label,
|
||||
)
|
||||
}
|
||||
MediumTonalButton(
|
||||
onClick = onClick,
|
||||
icon = icon,
|
||||
contentDescription = label,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = label,
|
||||
|
||||
@@ -189,4 +189,56 @@ object ReadConfig {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user