Очки: единый скролл диалога на оба глаза + мгновенная пауза звука телефона при вызове ассистента

This commit is contained in:
2026-08-23 21:08:40 +03:00
parent 706cc10d1e
commit eaca47c443
3 changed files with 90 additions and 26 deletions
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
@@ -49,12 +50,43 @@ fun AssistantOverlay() {
val chatHistory by app.chatHistory.collectAsState()
val state = AssistantState(listening, thinking, sttText, assistantText)
if (!overlayVisible(state)) return
overlayBox(app, state, chatHistory)
// Скролл и эффекты живут ВЫШЕ Binocular: Binocular рендерит контент дважды
// (левый/правый глаз), поэтому scrollState и коллекторы должны существовать
// в единственном экземпляре — иначе глаза скроллят по-разному.
val scrollState = rememberScrollState()
val hasHistory = chatHistory.isNotEmpty()
// Новый ответ (ShowText) или пополнившаяся история — к последнему сообщению.
LaunchedEffect(chatHistory.size, state.assistantText) {
if (hasHistory || state.assistantText.isNotBlank()) {
scrollState.animateScrollTo(scrollState.maxValue)
}
}
// Накопленная дельта скролла от жестов. Коллектор один на оба глаза;
// аккумулятор (answerScrollDelta) общий в GlassesApp. Цель — чистый [scrollOffset].
LaunchedEffect(Unit) {
app.answerScrollDelta.collect { pending ->
if (pending == 0f) return@collect
val target = scrollOffset(scrollState.value, pending, scrollState.maxValue)
app.answerScrollDelta.value = 0f
if (target != scrollState.value) {
scrollState.animateScrollTo(target, tween(150))
}
}
}
overlayBox(app, state, chatHistory, scrollState)
}
/** Поглазник: тот же scale/offset-контейнер, что у экранов, но с затемнением вместо чёрного. */
@Composable
private fun overlayBox(app: GlassesApp, state: AssistantState, chatHistory: List<ChatEntry>) {
private fun overlayBox(
app: GlassesApp,
state: AssistantState,
chatHistory: List<ChatEntry>,
scrollState: ScrollState,
) {
Binocular(scale = false) {
val scale by app.scale.collectAsState()
val screenWidthPx = BinocularScreenSize.current.width
@@ -69,38 +101,20 @@ private fun overlayBox(app: GlassesApp, state: AssistantState, chatHistory: List
.offset(x = scaleOffsetX(side, screenWidthPx, scale).px)
.background(Color.Black.copy(alpha = 0.72f)),
) {
AssistantOverlayContent(state, chatHistory)
AssistantOverlayContent(state, chatHistory, scrollState)
}
}
}
@Composable
private fun AssistantOverlayContent(state: AssistantState, chatHistory: List<ChatEntry>) {
val app = GlassesApp.instance
private fun AssistantOverlayContent(
state: AssistantState,
chatHistory: List<ChatEntry>,
scrollState: ScrollState,
) {
val hasHistory = chatHistory.isNotEmpty()
val fallbackAnswer = chatHistory.isEmpty() && state.assistantText.isNotBlank()
val header = overlayHeader(state)
val scrollState = rememberScrollState()
// Новый ответ (ShowText) или пополнившаяся история — к последнему сообщению.
LaunchedEffect(chatHistory.size, state.assistantText) {
if (hasHistory || state.assistantText.isNotBlank()) {
scrollState.animateScrollTo(scrollState.maxValue)
}
}
// Накопленная дельта скролла от жестов: каждый глаз — свой [LaunchedEffect],
// аккумулятор (answerScrollDelta) общий в GlassesApp. Цель — чистый [scrollOffset].
LaunchedEffect(Unit) {
app.answerScrollDelta.collect { pending ->
if (pending == 0f) return@collect
val target = scrollOffset(scrollState.value, pending, scrollState.maxValue)
app.answerScrollDelta.value = 0f
if (target != scrollState.value) {
scrollState.animateScrollTo(target, tween(150))
}
}
}
Column(
modifier = Modifier