CICD: сборка APK на release → F-Droid; versionCode из тега; фикс скролла каталога и масштаба
build / Android APK to F-Droid (release) Failing after 36s
build / Android APK to F-Droid (release) Failing after 36s
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
name: build
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
android:
|
||||
name: Android APK to F-Droid
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Java 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: adopt
|
||||
|
||||
- name: Setup Android SDK
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export ANDROID_HOME=$HOME/android-sdk
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
||||
curl -sSL -o /tmp/cmdtools.zip https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
||||
unzip -q -o /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||
fi
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-35" "build-tools;36.0.0" > /dev/null
|
||||
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build APKs
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew :app-phone:assembleRelease :app-glasses:assembleRelease \
|
||||
-PversionCode=${{ gitea.ref_name }} \
|
||||
--no-daemon --stacktrace
|
||||
|
||||
- name: Publish phone to F-Droid
|
||||
uses: https://git.binom.pw/subochev/devops/fdroid-publish@main
|
||||
with:
|
||||
apk_path: "app-phone/build/outputs/apk/release/app-phone-release-unsigned.apk"
|
||||
|
||||
- name: Publish glasses to F-Droid
|
||||
uses: https://git.binom.pw/subochev/devops/fdroid-publish@main
|
||||
with:
|
||||
apk_path: "app-glasses/build/outputs/apk/release/app-glasses-release-unsigned.apk"
|
||||
@@ -0,0 +1,106 @@
|
||||
# Задача: масштаб на всех экранах очков + сохранение скролла каталога
|
||||
|
||||
## Баг 1: ConnectionScreen игнорирует масштаб
|
||||
|
||||
**Симптом (пользователь):** «в режиме НЕ просмотра видео (пока ничего не включено)
|
||||
очки игнорируют выставленный масштаб». Плюс «масштаб будто бы не сохраняется —
|
||||
сразу после старта масштаб 1».
|
||||
|
||||
**Корень:** масштаб (`GlassesApp.instance.scale`, 0.5..1) применяется ТОЛЬКО в
|
||||
`GlassesMovieScreen.kt` (контейнер `width((screenWidthPx * scale).toInt().px)` +
|
||||
offset к центру). А `ConnectionScreen()` в
|
||||
`app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt` (строка ~354)
|
||||
рисует контент на ПОЛНУЮ половину глаза (`Modifier.width(screenWidthPx.px)`) —
|
||||
scale не читает вообще. Поэтому и «не сохраняется»: сохранённый scale в prefs есть,
|
||||
но на не-видео экране он не виден.
|
||||
|
||||
**Фикс:** в `ConnectionScreen()` применять scale так же, как в GlassesMovieScreen:
|
||||
|
||||
```kotlin
|
||||
fun ConnectionScreen() {
|
||||
val connection = GlassesApp.instance.hostConnection
|
||||
val connected by connection.connected.collectAsState()
|
||||
val status by connection.status.collectAsState()
|
||||
val messages by connection.messages.collectAsState()
|
||||
val scale by GlassesApp.instance.scale.collectAsState() // ← НОВОЕ
|
||||
|
||||
Binocular(scale = false) {
|
||||
val screenWidthPx = BinocularScreenSize.current.width
|
||||
val screenHeightPx = BinocularScreenSize.current.height
|
||||
// масштаб через layout — как в GlassesMovieScreen
|
||||
val containerW = (screenWidthPx * scale).toInt()
|
||||
val containerH = (screenHeightPx * scale).toInt()
|
||||
val offsetX = (screenWidthPx * (1f - scale)).toInt() // сдвиг к носу (левый глаз)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(containerW.px)
|
||||
.height(containerH.px)
|
||||
.offset(x = offsetX.px)
|
||||
.background(Color.Black),
|
||||
) {
|
||||
... существующий контент ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Внутри контейнера всё остаётся: Column с текстами, StatusPanelCompose,
|
||||
оверлей распознавания — но всё это теперь в уменьшенном контейнере.
|
||||
Импорт `androidx.compose.foundation.layout.offset` и `width/height` уже есть.
|
||||
|
||||
⚠️ ВАЖНО: `Modifier.offset(x=...)` — смещение внутри родителя. Внутри
|
||||
`Binocular(scale=false)` контент рисуется на половину 640. При scale<1 контейнер
|
||||
уже (например 448 при 0.7) и сдвинут к центру (к носу): offsetX = 640*(1-0.7)=192.
|
||||
Для левого глаза сдвиг вправо (к центру 640), правый глаз — как в GlassesMovieScreen
|
||||
(offset 0, контейнер прижат к центру слева). Смотри `scaleOffsetX` в
|
||||
GlassesMovieScreen.kt:143 — скопировать ту же логику (side из BinocularContent,
|
||||
там есть `Side.LEFT/RIGHT` через CompositionLocal).
|
||||
|
||||
## Баг 2: скролл каталога теряется при навигации
|
||||
|
||||
**Симптом (пользователь):** «кручу вниз до нужной папки, захожу, тыкаю назад
|
||||
(кнопку или аппаратную) и я оказываюсь в предыдущем окне в самом начале (скролл
|
||||
не сохранился). Надо сохранять скролл, даже если зашёл в папку, потом ещё в
|
||||
папку, потом ещё — и т.д.»
|
||||
|
||||
**Корень:** `app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt`:
|
||||
`LazyVerticalGrid` (строка 220) БЕЗ `LazyGridState` — позиция не хранится.
|
||||
`CatalogState` живёт на корне (переживает переходы), но позицию скролла не помнит.
|
||||
|
||||
**Фикс:** добавить в `CatalogState`:
|
||||
- `private var scrollIndex by mutableStateOf(0)` / `scrollOffset by mutableStateOf(0)` — текущая позиция
|
||||
- `private val savedScroll = mutableMapOf<Path, Pair<Int, Int>>()` — сохранённые позиции по пути
|
||||
- в `openSeries`/`openSeason`/`openFolder`: **сначала** `savedScroll[path] = scrollIndex to scrollOffset`, **потом** менять path
|
||||
- в `back()`: после смены path — восстановить `(scrollIndex, scrollOffset) = savedScroll[path] ?: (0,0)`
|
||||
- методы `setScroll(index, offset)` (для snapshotFlow из UI) и `getSavedScroll(): Pair<Int,Int>` (текущий path)
|
||||
|
||||
В `CatalogScreen` composable:
|
||||
```kotlin
|
||||
val gridState = rememberLazyGridState()
|
||||
// восстанавливаем позицию при смене пути/загрузке
|
||||
LaunchedEffect(state.path, state.items) {
|
||||
val (idx, off) = state.savedScrollForCurrentPath()
|
||||
gridState.scrollToItem(idx, off)
|
||||
}
|
||||
// сохраняем позицию при скролле
|
||||
LaunchedEffect(gridState) {
|
||||
snapshotFlow {
|
||||
gridState.firstVisibleItemIndex to gridState.firstVisibleItemScrollOffset
|
||||
}.collect { (idx, off) -> state.setScroll(idx, off) }
|
||||
}
|
||||
```
|
||||
и `LazyVerticalGrid(state = gridState, ...)`.
|
||||
|
||||
Ключ сохранения — РОДИТЕЛЬСКИЙ path (тот, откуда ушли). При back() path меняется
|
||||
на родителя — достаём сохранённое. Вложенные папки работают автоматически:
|
||||
каждый уровень сохраняет свою позицию.
|
||||
|
||||
Импорты: `androidx.compose.foundation.lazy.grid.rememberLazyGridState`,
|
||||
`androidx.compose.runtime.snapshotFlow`.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
1. Сборка: `./gradlew :app-phone:assembleDebug :app-glasses:assembleDebug` — успех.
|
||||
2. Юнит-тесты: `./gradlew :lib-core:testDebugUnitTest :app-phone:testDebugUnitTest :app-glasses:testDebugUnitTest` — зелёные.
|
||||
3. Ручная проверка на очках: выставить масштаб 0.7 → ConnectionScreen (без видео) тоже уменьшен (чёрные поля по краям, картинка/текст сдвинуты к носу).
|
||||
4. Ручная проверка: скролл каталога вниз → зайти в папку → назад → позиция на месте; то же для вложенных папок.
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "pw.binom.viewmate.glasses"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionCode = (project.findProperty("versionCode") as? String)?.toIntOrNull() ?: 1
|
||||
versionName = (project.findProperty("versionName") as? String) ?: "0.1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -45,6 +45,9 @@ class GlassesApp : Application() {
|
||||
/** Текст ответа ассистента (обновляется на ShowText); остаётся до новой сессии. */
|
||||
val assistantText = MutableStateFlow("")
|
||||
|
||||
/** Текущая Activity (для закрытия приложения по команде с телефона). */
|
||||
var mainActivity: MainActivity? = null
|
||||
|
||||
/** Применить и сохранить масштаб. */
|
||||
fun applyScale(value: Float) {
|
||||
val v = value.coerceIn(0.5f, 1f)
|
||||
|
||||
@@ -31,6 +31,7 @@ import pw.binom.viewmate.core.protocol.PlayVideo
|
||||
import pw.binom.viewmate.core.protocol.PlaybackPosition
|
||||
import pw.binom.viewmate.core.protocol.SetMode
|
||||
import pw.binom.viewmate.core.protocol.SetScale
|
||||
import pw.binom.viewmate.core.protocol.Shutdown
|
||||
import pw.binom.viewmate.core.protocol.ShowText
|
||||
import pw.binom.viewmate.core.protocol.SttCancel
|
||||
import pw.binom.viewmate.core.protocol.SttDone
|
||||
@@ -288,6 +289,14 @@ class HostConnection(
|
||||
log("stt", "отменено")
|
||||
GlassesApp.instance.listening.value = false
|
||||
}
|
||||
|
||||
is Shutdown -> {
|
||||
addMessage("Shutdown: закрываю приложение")
|
||||
// как в pair-view: finish() Activity очков
|
||||
GlassesApp.instance.mainActivity?.finishAffinity()
|
||||
// страховка: процесс завершится сам, но kill убирает зависшие потоки (RayNeo)
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.lazy.LazyColumn
|
||||
@@ -56,9 +56,12 @@ import pw.binom.viewmate.glasses.gestures.templeActionToCommand
|
||||
import pw.binom.viewmate.glasses.status.StatusPanelCompose
|
||||
import pw.binom.viewmate.glasses.stt.SttMicStream
|
||||
import pw.binom.viewmate.glasses.ui.Binocular
|
||||
import pw.binom.viewmate.glasses.ui.BinocularScreenSide
|
||||
import pw.binom.viewmate.glasses.ui.BinocularScreenSize
|
||||
import pw.binom.viewmate.glasses.ui.GlassesMovieScreen
|
||||
import pw.binom.viewmate.glasses.ui.Side
|
||||
import pw.binom.viewmate.glasses.ui.px
|
||||
import pw.binom.viewmate.glasses.ui.scaleOffsetX
|
||||
|
||||
class MainActivity : BaseEventActivity() {
|
||||
private var resumeAfterScreenOn = false
|
||||
@@ -112,6 +115,7 @@ class MainActivity : BaseEventActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
GlassesApp.instance.mainActivity = this
|
||||
collectTempleActions()
|
||||
disableTrackpadSounds()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@@ -311,6 +315,7 @@ class MainActivity : BaseEventActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
GlassesApp.instance.mainActivity = null
|
||||
super.onDestroy()
|
||||
unregisterReceiver(screenReceiver)
|
||||
unregisterReceiver(debugReceiver)
|
||||
@@ -354,12 +359,27 @@ fun ConnectionScreen() {
|
||||
val connected by connection.connected.collectAsState()
|
||||
val status by connection.status.collectAsState()
|
||||
val messages by connection.messages.collectAsState()
|
||||
val scale by GlassesApp.instance.scale.collectAsState()
|
||||
|
||||
// Бинокулярно: контент дублируется в оба глаза, панель — по центру каждой
|
||||
// половины (явная ширина половины, иначе fillMaxSize растянет на 1280).
|
||||
Binocular(scale = false) {
|
||||
val screenWidthPx = BinocularScreenSize.current.width
|
||||
Box(modifier = Modifier.width(screenWidthPx.px).fillMaxHeight().background(Color.Black)) {
|
||||
val screenHeightPx = BinocularScreenSize.current.height
|
||||
val side = BinocularScreenSide.current
|
||||
|
||||
// Масштаб через layout: контейнер каждого глаза — scale от половины экрана,
|
||||
// сдвинут к центру (к носу) на (1 - scale) своей ширины.
|
||||
val containerW = (screenWidthPx * scale).toInt()
|
||||
val containerH = (screenHeightPx * scale).toInt()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(containerW.px)
|
||||
.height(containerH.px)
|
||||
.offset(x = scaleOffsetX(side, screenWidthPx, scale).px)
|
||||
.background(Color.Black),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 56.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -377,6 +397,7 @@ fun ConnectionScreen() {
|
||||
|
||||
StatusPanelCompose(
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 12.dp),
|
||||
scale = scale,
|
||||
)
|
||||
|
||||
// Оверлей распознавания: затемнение + накопленный текст (поверх всего).
|
||||
|
||||
@@ -37,8 +37,8 @@ android {
|
||||
applicationId = "pw.binom.viewmate.phone"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionCode = (project.findProperty("versionCode") as? String)?.toIntOrNull() ?: 1
|
||||
versionName = (project.findProperty("versionName") as? String) ?: "0.1.0"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// LLM-ключ из local.properties (llm.apiKey) — вне git; пустой = ещё не задан
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -77,6 +79,13 @@ class CatalogState(
|
||||
private set
|
||||
var path by mutableStateOf<Path>(Path.Root)
|
||||
private set
|
||||
var scrollIndex by mutableStateOf(0)
|
||||
private set
|
||||
var scrollOffset by mutableStateOf(0)
|
||||
private set
|
||||
|
||||
/** Сохранённые позиции скролла по пути (ключ — путь, из которого ушли). */
|
||||
private val savedScroll = mutableMapOf<CatalogState.Path, Pair<Int, Int>>()
|
||||
|
||||
private var searchJob: Job? = null
|
||||
private var loadJob: Job? = null
|
||||
@@ -102,16 +111,19 @@ class CatalogState(
|
||||
}
|
||||
|
||||
fun openSeries(item: JellyfinItem) {
|
||||
savedScroll[path] = scrollIndex to scrollOffset
|
||||
path = Path.Series(item)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun openSeason(season: JellyfinItem, series: JellyfinItem) {
|
||||
savedScroll[path] = scrollIndex to scrollOffset
|
||||
path = Path.Season(season, series)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun openFolder(item: JellyfinItem) {
|
||||
savedScroll[path] = scrollIndex to scrollOffset
|
||||
path = Path.Folder(item)
|
||||
refresh()
|
||||
}
|
||||
@@ -123,9 +135,27 @@ class CatalogState(
|
||||
is Path.Season -> Path.Series(p.series)
|
||||
is Path.Folder -> Path.Root
|
||||
}
|
||||
val (idx, off) = savedScroll[path] ?: Pair(0, 0)
|
||||
scrollIndex = idx
|
||||
scrollOffset = off
|
||||
refresh()
|
||||
}
|
||||
|
||||
/** Сохранить позицию для текущего пути (перед уходом на детали и т.п.). */
|
||||
fun saveScrollNow() {
|
||||
savedScroll[path] = scrollIndex to scrollOffset
|
||||
}
|
||||
|
||||
/** Сохранить текущую позицию (вызывается при скролле из UI). */
|
||||
fun setScroll(index: Int, offset: Int) {
|
||||
scrollIndex = index
|
||||
scrollOffset = offset
|
||||
}
|
||||
|
||||
/** Позиция скролла для текущего пути (восстановление после навигации). */
|
||||
fun savedScrollForCurrentPath(): Pair<Int, Int> =
|
||||
savedScroll[path] ?: Pair(0, 0)
|
||||
|
||||
/** Заголовок под шапкой: текущая вкладка или хлебная крошка внутри сериала/папки. */
|
||||
fun subtitle(): String = when (val p = path) {
|
||||
Path.Root -> when (tab) {
|
||||
@@ -200,6 +230,21 @@ fun CatalogScreen(
|
||||
) {
|
||||
val app = PhoneApp.instance
|
||||
val userId by app.userId.collectAsState()
|
||||
|
||||
// Позиция скролла каталога: сохраняется/восстанавливается между навигациями.
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
// Восстановить позицию при смене пути/загрузке.
|
||||
LaunchedEffect(state.path, state.items) {
|
||||
val (idx, off) = state.savedScrollForCurrentPath()
|
||||
gridState.scrollToItem(idx, off)
|
||||
}
|
||||
// Сохранять позицию при скролле.
|
||||
LaunchedEffect(gridState) {
|
||||
snapshotFlow {
|
||||
gridState.firstVisibleItemIndex to gridState.firstVisibleItemScrollOffset
|
||||
}.collect { (idx, off) -> state.setScroll(idx, off) }
|
||||
}
|
||||
val downloaded by app.state.downloadedFlow.collectAsState()
|
||||
val localDownloaded by app.downloadManager.downloadedIds.collectAsState()
|
||||
|
||||
@@ -218,6 +263,7 @@ fun CatalogScreen(
|
||||
ScreenPlaceholder(if (state.query.isNotBlank()) "ничего не нашлось" else "пусто")
|
||||
} else {
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(14.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
@@ -246,7 +292,11 @@ fun CatalogScreen(
|
||||
state.openSeason(item, series)
|
||||
}
|
||||
item.Type in CatalogState.CONTAINER_TYPES -> state.openFolder(item)
|
||||
else -> onItemClick(item)
|
||||
else -> {
|
||||
// уходим на детали — сохранить позицию, чтобы назад вернул скролл
|
||||
state.saveScrollNow()
|
||||
onItemClick(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -213,6 +213,15 @@ fun GlassesScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
RemoteButton(
|
||||
text = "⏻ Выключить очки",
|
||||
big = true,
|
||||
onClick = {
|
||||
scope.launch { app.actions?.shutdownGlasses() }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp),
|
||||
)
|
||||
|
||||
RemoteButton(
|
||||
text = "Скачанное на очках →",
|
||||
big = true,
|
||||
|
||||
@@ -18,6 +18,7 @@ import pw.binom.viewmate.core.protocol.PlayVideo
|
||||
import pw.binom.viewmate.core.protocol.SetMode
|
||||
import pw.binom.viewmate.core.protocol.SetScale
|
||||
import pw.binom.viewmate.core.protocol.ShowText
|
||||
import pw.binom.viewmate.core.protocol.Shutdown
|
||||
|
||||
/**
|
||||
* Действия «телефона»: каталог Jellyfin, зеркала media-mirror, очки.
|
||||
@@ -267,5 +268,11 @@ class PhoneActions(
|
||||
return "масштаб → $clamped"
|
||||
}
|
||||
|
||||
/** Закрыть приложение на очках (телефон остаётся открытым). */
|
||||
suspend fun shutdownGlasses(): String {
|
||||
sender.send(Shutdown())
|
||||
return "выключение очков"
|
||||
}
|
||||
|
||||
private fun ticksToMinutes(ticks: Long): Long = ticks / 10_000_000 / 60
|
||||
}
|
||||
|
||||
@@ -95,3 +95,8 @@ data class SttDone(val full: String) : HostToGlasses
|
||||
@Serializable
|
||||
@SerialName("stt_cancel")
|
||||
class SttCancel : HostToGlasses
|
||||
|
||||
/** Полное выключение приложения на очках (по команде с телефона). */
|
||||
@Serializable
|
||||
@SerialName("shutdown")
|
||||
class Shutdown : HostToGlasses
|
||||
|
||||
Reference in New Issue
Block a user