Skip to content

Commit f6890c2

Browse files
committed
Persist TTS voice and improve display
1 parent c0f2106 commit f6890c2

File tree

12 files changed

+102
-23
lines changed

12 files changed

+102
-23
lines changed

composeApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
33
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
44

5-
val version = "1.0.42"
5+
val version = "1.0.43"
66
val versionNumber = getVersionInt()
77

88
plugins {

composeApp/src/androidMain/kotlin/platformSpecific/tts/TtsHelperImpl.kt

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import android.media.AudioManager
77
import android.speech.tts.TextToSpeech
88
import android.util.Log
99
import com.adriantache.ContextProvider
10-
import kotlinx.coroutines.CoroutineScope
11-
import kotlinx.coroutines.Dispatchers
12-
import kotlinx.coroutines.delay
10+
import data.settings.SettingsRepositoryImpl
11+
import kotlinx.coroutines.*
1312
import kotlinx.coroutines.flow.MutableStateFlow
1413
import kotlinx.coroutines.flow.StateFlow
15-
import kotlinx.coroutines.launch
1614
import platformSpecific.tts.model.TtsVoice
1715
import java.util.*
1816

@@ -22,6 +20,7 @@ object TtsHelperImpl : TtsHelper {
2220
private val audioManager: AudioManager
2321
private val audioFocusRequest
2422
get() = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT).build()
23+
private val settingsRepository = SettingsRepositoryImpl() // TODO: definitely don't access this like this
2524

2625
init {
2726
val context = requireNotNull(ContextProvider.context.get())
@@ -41,19 +40,23 @@ object TtsHelperImpl : TtsHelper {
4140
}
4241

4342
override fun setVoice(voice: TtsVoice) {
44-
val expectedVoice = tts.voices.find { it.name == voice.name }
43+
val expectedVoice = tts.voices.find { it.name == voice.id }
4544

4645
if (expectedVoice == null) Log.e(this::class.simpleName, "Cannot find voice $voice!")
46+
else runBlocking { settingsRepository.setTtsVoice(voice.id) }
4747

4848
tts.voice = expectedVoice ?: tts.defaultVoice
4949
}
5050

5151
override fun getVoice(): TtsVoice? {
52-
return tts.voice?.let { TtsVoice(it.name) }
52+
return tts.voice?.let { getTtsVoice(it.name) }
5353
}
5454

5555
override fun getVoices(): List<TtsVoice> {
56-
return tts.voices?.map { TtsVoice(it.name) }.orEmpty()
56+
return tts.voices.orEmpty()
57+
.sortedBy { it.name }
58+
.map { getTtsVoice(it.name) }
59+
.filter { it.locale?.language == "en" }
5760
}
5861

5962
private fun getTts(context: Context): TextToSpeech {
@@ -71,9 +74,17 @@ object TtsHelperImpl : TtsHelper {
7174
tts.apply {
7275
setPitch(1f)
7376
setSpeechRate(1f)
74-
setLanguage(Locale.US).apply {
75-
if (this in listOf(TextToSpeech.LANG_MISSING_DATA, TextToSpeech.LANG_NOT_SUPPORTED)) {
76-
Log.e("TTS", "This Language is not supported")
77+
78+
runBlocking { // TODO: really improve this part...
79+
val settings = settingsRepository.getSettings()
80+
if (settings.ttsVoice != null) {
81+
voice = voices.find { it.name == settings.ttsVoice }
82+
} else {
83+
setLanguage(Locale.US).apply {
84+
if (this in listOf(TextToSpeech.LANG_MISSING_DATA, TextToSpeech.LANG_NOT_SUPPORTED)) {
85+
Log.e("TTS", "This Language is not supported")
86+
}
87+
}
7788
}
7889
}
7990
}
@@ -97,4 +108,20 @@ object TtsHelperImpl : TtsHelper {
97108

98109
return statusFlow
99110
}
111+
112+
private fun getTtsVoice(id: String): TtsVoice {
113+
val sections = id.split("-")
114+
val locale = Locale(
115+
sections[0],
116+
sections[1]
117+
)
118+
val description = sections.drop(2).filter { it.length > 1 }.joinToString(" ")
119+
val name = "${locale.displayName} $description"
120+
121+
return TtsVoice(
122+
id = id,
123+
name = name,
124+
locale = locale,
125+
)
126+
}
100127
}

composeApp/src/commonMain/kotlin/data/settings/SettingsRepositoryImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ class SettingsRepositoryImpl(
1616
override suspend fun setApiKey(key: String?) {
1717
settingsDataSource.setApiKey(key)
1818
}
19+
20+
override suspend fun setTtsVoice(voiceId: String?) {
21+
settingsDataSource.setTtsVoice(voiceId)
22+
}
1923
}

composeApp/src/commonMain/kotlin/data/settings/dataSource/SettingsDataSource.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ interface SettingsDataSource {
66
suspend fun getSettings(): SettingsData
77

88
suspend fun setApiKey(key: String?)
9+
10+
suspend fun setTtsVoice(voiceId: String?)
911
}

composeApp/src/commonMain/kotlin/data/settings/dataSource/SettingsDataSourceImpl.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import domain.settings.data.model.SettingsData
88
import kotlinx.coroutines.flow.firstOrNull
99

1010
private val API_KEY_KEY = stringPreferencesKey("API_KEY_KEY_NEW")
11+
private val TTS_VOICE_KEY = stringPreferencesKey("TTS_VOICE_KEY")
1112

1213
class SettingsDataSourceImpl(
1314
private val store: DataStore<Preferences> = DataStoreHelper.instance,
1415
) : SettingsDataSource {
1516
override suspend fun getSettings(): SettingsData {
1617
val apiKey = store.data.firstOrNull()?.get(API_KEY_KEY)
18+
val ttsVoice = store.data.firstOrNull()?.get(TTS_VOICE_KEY)
1719

1820
return SettingsData(
1921
apiKey = apiKey,
22+
ttsVoice = ttsVoice,
2023
)
2124
}
2225

@@ -31,4 +34,16 @@ class SettingsDataSourceImpl(
3134
}
3235
}
3336
}
37+
38+
override suspend fun setTtsVoice(voiceId: String?) {
39+
store.updateData {
40+
it.toMutablePreferences().apply {
41+
if (voiceId == null) {
42+
remove(TTS_VOICE_KEY)
43+
} else {
44+
set(TTS_VOICE_KEY, voiceId)
45+
}
46+
}
47+
}
48+
}
3449
}

composeApp/src/commonMain/kotlin/domain/settings/data/SettingsRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ interface SettingsRepository {
66
suspend fun getSettings(): SettingsData
77

88
suspend fun setApiKey(key: String?)
9+
10+
suspend fun setTtsVoice(voiceId: String?)
911
}

composeApp/src/commonMain/kotlin/domain/settings/data/model/SettingsData.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package domain.settings.data.model
22

33
data class SettingsData(
44
val apiKey: String?,
5+
val ttsVoice: String?,
56
)
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
package platformSpecific.tts.model
22

3-
data class TtsVoice(val name: String)
3+
import java.util.*
4+
5+
data class TtsVoice(
6+
val id: String,
7+
val name: String?,
8+
val locale: Locale?
9+
)

composeApp/src/commonMain/kotlin/presentation/navigation/view/SettingsScreen.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.runtime.*
88
import androidx.compose.ui.Modifier
99
import androidx.compose.ui.unit.dp
1010
import androidx.compose.ui.window.Dialog
11+
import androidx.compose.ui.window.DialogProperties
1112
import domain.settings.state.SettingsState
1213
import platformSpecific.tts.getTtsHelper
1314
import presentation.settings.tts.TtsVoiceSelectionView
@@ -44,7 +45,10 @@ fun SettingsScreen(
4445
)
4546

4647
if (displayTtsSelector) {
47-
Dialog(onDismissRequest = { displayTtsSelector = false }) {
48+
Dialog(
49+
onDismissRequest = { displayTtsSelector = false },
50+
properties = DialogProperties(usePlatformDefaultWidth = false),
51+
) {
4852
TtsVoiceSelectionView(
4953
modifier = Modifier.padding(16.dp),
5054
onDismiss = { displayTtsSelector = false },

composeApp/src/commonMain/kotlin/presentation/settings/tts/TtsVoiceSelectionView.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.material.MaterialTheme
1010
import androidx.compose.material.RadioButton
1111
import androidx.compose.material.Text
1212
import androidx.compose.runtime.*
13+
import androidx.compose.ui.Alignment
1314
import androidx.compose.ui.Modifier
1415
import androidx.compose.ui.unit.dp
1516
import platformSpecific.tts.getTtsHelper
@@ -21,7 +22,7 @@ fun TtsVoiceSelectionView(
2122
modifier: Modifier = Modifier,
2223
onDismiss: () -> Unit,
2324
) {
24-
val ttsHelper by remember { lazy { getTtsHelper() } }
25+
val ttsHelper by remember { lazy { getTtsHelper() } } // TODO: replace direct access with usecase
2526
var selectedTtsVoice: TtsVoice? by remember { mutableStateOf(null) }
2627
var voices: List<TtsVoice> by remember { mutableStateOf(emptyList()) }
2728

@@ -36,7 +37,7 @@ fun TtsVoiceSelectionView(
3637
ttsHelper?.let { ttsHelper ->
3738
selectedTtsVoice = voice
3839
ttsHelper.setVoice(voice)
39-
ttsHelper.speak("Hello! This is ${voice.name} speaking.")
40+
ttsHelper.speak("Hello! This is ${voice.id} speaking.")
4041
} ?: onDismiss()
4142
}
4243

@@ -64,16 +65,28 @@ fun VoiceRow(
6465
.padding(16.dp),
6566
horizontalArrangement = Arrangement.SpaceBetween
6667
) {
67-
Row {
68+
Row(
69+
verticalAlignment = Alignment.CenterVertically,
70+
) {
6871
RadioButton(
6972
selected = selected,
7073
onClick = null,
7174
)
7275
Spacer(modifier = Modifier.width(8.dp))
7376
Column {
74-
Text(text = voice.name, style = MaterialTheme.typography.subtitle1, color = AppColor.onCard())
75-
Text(text = voice.name, style = MaterialTheme.typography.caption, color = AppColor.onCard())
77+
Text(
78+
text = voice.name ?: voice.id,
79+
style = MaterialTheme.typography.subtitle1,
80+
color = AppColor.onCard()
81+
)
82+
Text(text = voice.id, style = MaterialTheme.typography.caption, color = AppColor.onCard())
7683
}
84+
Spacer(modifier = Modifier.width(8.dp))
85+
Text(
86+
text = voice.locale?.displayName.orEmpty(),
87+
style = MaterialTheme.typography.subtitle2,
88+
color = AppColor.onCard()
89+
)
7790
}
7891
}
7992
Divider()

0 commit comments

Comments
 (0)