ui(phone): переключение сессий ступенчатым свайпом (порог 120dp, ±1 сессия за жест) + крупнее текст сообщений (14→20sp)

This commit is contained in:
2026-08-27 18:46:31 +03:00
parent 236ff18543
commit cf8c5ef604
@@ -1,6 +1,7 @@
package pw.binom.viewmate.phone.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -33,6 +34,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -48,7 +51,9 @@ import pw.binom.viewmate.phone.agent.Session
/**
* Экран «Чат с Порфирием»: история из SQLite, ввод текста, отправка в
* локального ассистента (та же очередь, что и голос с очков). Управление
* сессиями: новая, переключение (выпадающий список), сброс текущей.
* сессиями: новая, переключение (выпадающий список ИЛИ ступенчатый горизонтальный
* свайп ленты сообщений — на одну сессию за жест, без инерции; TZ-ui-four п.3),
* сброс текущей.
*/
@Composable
fun ChatScreen() {
@@ -104,6 +109,25 @@ fun ChatScreen() {
}
}
// Ступенчатое переключение сессий свайпом (TZ-ui-four п.3): фиксированный
// порог смещения в px (плотность экрана).
val thresholdPx = with(LocalDensity.current) { CHAT_SESSION_SWIPE_THRESHOLD.toPx() }
/**
* Ступенчатое переключение: ровно ОДИН шаг (+1 — следующая, −1 — предыдущая),
* без перескока сразу на несколько сессий, без инерции/флинга;
* за границей списка — ничего.
*/
fun switchSessionBy(delta: Int) {
if (sessions.size < 2) return
val idx = sessions.indexOfFirst { it.id == activeId }.let { if (it >= 0) it else 0 }
val target = idx + delta
if (target < 0 || target >= sessions.size) return
val s = sessions[target]
activeId = s.id
app.selectChatSession(s.id)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -173,7 +197,31 @@ fun ChatScreen() {
Spacer(Modifier.height(4.dp))
// Лента сообщений
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
// Ступенчатая смена сессии (TZ-ui-four п.3): горизонтальный свайп
// ленты за фиксированный порог = ±одна сессия (свайп влево —
// следующая, вправо — предыдущая), мгновенно, без физики/инерции.
// foundation 1.8: onHorizontalDrag даёт (change, суммарный сдвиг x с
// начала жеста) — берём суммарный сдвиг без накопления по кадрам.
.pointerInput(sessions.size) {
var draggedPx = 0f
detectHorizontalDragGestures(
onHorizontalDrag = { _, amount -> draggedPx = amount },
onDragCancel = { draggedPx = 0f },
onDragEnd = {
when {
draggedPx <= -thresholdPx -> switchSessionBy(1)
draggedPx >= thresholdPx -> switchSessionBy(-1)
else -> Unit
}
draggedPx = 0f
},
)
},
) {
if (messages.isEmpty()) {
Text(
text = "Спроси что-нибудь — например, «сколько сейчас время?»",
@@ -228,7 +276,9 @@ private fun MessageBubble(role: ChatRole, text: String) {
Text(
text = text,
color = PhoneText,
fontSize = 14.sp,
// Базовый шрифт сообщений (TZ-ui-four п.2): крупнее, чтобы читалось
// без напряжения (14.sp → 20.sp, ~+43%); вёртка баллонов не менялась.
fontSize = 20.sp,
modifier = Modifier
.padding(horizontal = 10.dp, vertical = 6.dp)
.background(
@@ -239,3 +289,9 @@ private fun MessageBubble(role: ChatRole, text: String) {
)
}
}
/**
* Фиксированный порог ступенчатого свайпа переключения сессий (TZ-ui-four п.3):
* жест «перетолкал» ленту на него — смена ровно на одну сессию; меньше — назад.
*/
private val CHAT_SESSION_SWIPE_THRESHOLD = 120.dp