From e28be0301b0cb45634f83d681fdb6942cc2b350a Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Tue, 9 Jun 2026 02:48:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9C=97=E8=AF=BB=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ReadAloudSettingsRepository.kt | 57 +- .../io/legado/app/help/config/AppConfig.kt | 34 +- .../legado/app/help/config/ReadBookConfig.kt | 521 +++++------------- .../io/legado/app/service/AudioPlayService.kt | 9 +- .../app/service/BaseReadAloudService.kt | 25 +- .../app/service/HttpReadAloudService.kt | 7 +- .../legado/app/service/TTSReadAloudService.kt | 5 +- .../app/ui/book/read/ReadBookContract.kt | 12 + .../app/ui/book/read/ReadBookController.kt | 1 + .../legado/app/ui/book/read/ReadBookScreen.kt | 14 +- .../app/ui/book/read/ReadBookViewModel.kt | 86 ++- .../book/read/sheet/ReadAloudConfigSheet.kt | 286 +++++++++- .../app/ui/book/read/sheet/ReadAloudSheet.kt | 119 ++-- .../app/ui/config/readConfig/ReadConfig.kt | 52 ++ 14 files changed, 703 insertions(+), 525 deletions(-) diff --git a/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt b/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt index eadc16aeb..54a0d91ca 100644 --- a/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 32288a0e4..63777f192 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt index bef237980..51d4fffc3 100644 --- a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt @@ -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 > clampedPrefDelegate( + key: String, + defaultValue: T, + range: ClosedRange, + ): PrefDelegate { + val raw = prefDelegate(key, defaultValue) + return object : PrefDelegate { + override val state: State 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 存储为 JSON String 的 prefDelegate */ + private class PrefDelegateMap( + key: String, + private val parse: (String?) -> Map, + private val encode: (Map) -> String, + ) : ReadWriteProperty> { + private val raw = prefDelegate(key, null) + override fun getValue(thisRef: Any?, property: KProperty<*>): Map = + parse(raw.getValue(thisRef, property)) + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Map) = + 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 { + 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 { + if (value.isNullOrBlank()) return emptyMap() + return GSON.fromJsonObject>(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 by PrefDelegateMap( + PreferKey.readMenuCustomIcons, ::parseReadMenuCustomIcons, ::encodeReadMenuCustomIcons, + ) - var readMenuContainerColor: Int - get() = readMenuContainerColorValue.takeIf { it != 0 } ?: readMenuBgColor - set(value) { - readMenuContainerColorValue = value - } + var titleBarCustomIcons: Map 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 - get() = readMenuCustomIconsValue - set(value) { - readMenuCustomIconsValue = value.filterValues { it.isNotBlank() } - } - - var titleBarCustomIcons: Map - 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 { - return GSON.toJson(value.filterValues { it.isNotBlank() }) - } - - private fun parseReadMenuCustomIcons(value: String?): Map { - if (value.isNullOrBlank()) { - return emptyMap() - } - return GSON.fromJsonObject>(value).getOrNull() - ?.filterValues { it.isNotBlank() } - ?: emptyMap() - } - val regexColorRules: ArrayList 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 diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt index 7b13b2332..ea5a70356 100644 --- a/app/src/main/java/io/legado/app/service/AudioPlayService.kt +++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index 6a1516ee8..89ad5d62e 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -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 } diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 6c83ec29b..780ab6074 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt index bf93e315f..4983a4976 100644 --- a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -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) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt index 62bbfd2d8..445e3a5a0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt @@ -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 = 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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt index 485c7c93d..02ec69b8d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt index 50b4575ee..eac6bf344 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt @@ -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), diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 18a713a6f..b4d650e6e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -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()) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt index 6b808c81a..3d2d8a584 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt @@ -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, - 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)) } + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt index f6abeb9cd..6f74fe818 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt b/app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt index a7d0969f5..e012837b8 100644 --- a/app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt @@ -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 + ) }