stronger haptics + live language switching

Haptics: all feedback stronger and more distinct —
- click (send): 1.0 intensity CLICK (was 0.85)
- thinkingStart: double-tap TICK 0.5→0.7 (was single 0.22)
- responseStart: three-step crescendo TICK→CLICK→CLICK (firmer)
- responseEnd: firm CLICK + settling TICK (was soft 0.3 TICK)

Language: selectLocale() now applies immediately —
- API 33+: LocaleManager.setApplicationLocales (no restart)
- API <33: updateConfiguration + activity.recreate()
No new dependencies needed.
This commit is contained in:
Bruno Deanoz 2026-06-22 01:31:02 +02:00
parent 163a2cee28
commit e9e2a66b7d
3 changed files with 44 additions and 22 deletions

View file

@ -36,47 +36,53 @@ class Haptics(private val vibrator: Vibrator?) {
}
}
/** Firmer click — send button. */
/** Firmer click — send button, satisfying "message sent" confirmation. */
fun click() {
val vib = active() ?: return
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_CLICK)) {
vib.composePrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK to 0.85f)
vib.composePrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK to 1.0f)
} else {
vib.predefinedOrOneShot(soft = false)
}
}
/** Very soft — the model received the prompt and starts thinking. */
/** Double-tap — the model received the prompt and starts thinking. */
fun thinkingStart() {
val vib = active() ?: return
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) {
vib.composePrimitives(VibrationEffect.Composition.PRIMITIVE_TICK to 0.22f)
} else {
vib.predefinedOrOneShot(soft = true)
}
}
/** "The answer is arriving" — a three-step rising crescendo (the key moment). */
fun responseStart() {
val vib = active() ?: return
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) {
vib.composePrimitives(
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.30f, 0),
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.60f, 40),
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 1.00f, 40),
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0),
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.7f, 60),
)
} else {
vib.predefinedOrOneShot(soft = false)
}
}
/** Gentle settle — stream finished. */
/** Rising crescendo — first token arrived, answer is streaming. */
fun responseStart() {
val vib = active() ?: return
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_CLICK)) {
vib.composePrimitives(
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f, 0),
Triple(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 50),
Triple(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 50),
)
} else {
vib.predefinedOrOneShot(soft = false)
}
}
/** Firm settle — stream finished, answer complete. */
fun responseEnd() {
val vib = active() ?: return
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) {
vib.composePrimitives(VibrationEffect.Composition.PRIMITIVE_TICK to 0.3f)
if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_CLICK)) {
vib.composePrimitives(
Triple(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.8f, 0),
Triple(VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f, 80),
)
} else {
vib.predefinedOrOneShot(soft = true)
vib.predefinedOrOneShot(soft = false)
}
}

View file

@ -45,6 +45,7 @@ import dev.kaizen.app.ui.theme.themes.KaizenThemeId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import dev.kaizen.app.chat.MeshBackground
/**
@ -129,7 +130,8 @@ fun SettingsScreen(
SettingsOptionRow(title = stringResource(R.string.settings_language_title), description = stringResource(R.string.settings_language_desc), icon = Icons.Rounded.Language) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
listOf("system" to R.string.settings_language_system, "de" to R.string.settings_language_de, "en" to R.string.settings_language_en).forEach { (code, labelRes) ->
AppearanceTogglePill(selected = locale == code, label = stringResource(labelRes), onClick = { viewModel.selectLocale(code) })
val ctx = LocalContext.current
AppearanceTogglePill(selected = locale == code, label = stringResource(labelRes), onClick = { viewModel.selectLocale(code, ctx as? android.app.Activity) })
}
}
}

View file

@ -51,8 +51,22 @@ class SettingsViewModel(private val store: SecureStore? = null) : ViewModel() {
_userEmail.value = email
}
fun selectLocale(locale: String) {
fun selectLocale(locale: String, activity: android.app.Activity? = null) {
_locale.value = locale
store?.saveLocale(locale)
val localeList = if (locale == "system") {
android.os.LocaleList.getDefault()
} else {
android.os.LocaleList(java.util.Locale.forLanguageTag(locale))
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
activity?.getSystemService(android.app.LocaleManager::class.java)?.applicationLocales = localeList
} else {
val config = android.content.res.Configuration(activity?.resources?.configuration)
config.setLocales(localeList)
@Suppress("DEPRECATION")
activity?.resources?.updateConfiguration(config, activity.resources.displayMetrics)
activity?.recreate()
}
}
}