fix: 朗读相关
This commit is contained in:
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user