diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MediaAudioDeviceButton.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MediaAudioDeviceButton.kt new file mode 100644 index 00000000..55ace16d --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MediaAudioDeviceButton.kt @@ -0,0 +1,66 @@ +package com.utun.chat.ui.components + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.HeadsetMic +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.content.ContextCompat +import com.utun.chat.R +import com.utun.chat.data.MediaAudioController +import com.utun.chat.data.MediaAudioProfile +import com.utun.chat.data.LogManager + +/** Один выбор для панели рации, публикации и всех экранов просмотра. */ +@Composable +fun MediaAudioDeviceButton(tint: Color = LocalContentColor.current, onExpandedChange: (Boolean) -> Unit = {}) { + val state by MediaAudioController.state.collectAsState() + val context = LocalContext.current + var expanded by remember { mutableStateOf(false) } + val permission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) MediaAudioController.select(MediaAudioProfile.BLUETOOTH) + else LogManager.addLog("WARN", "media_audio", "Bluetooth selection cancelled: BLUETOOTH_CONNECT denied") + } + val bluetoothPermission = Build.VERSION.SDK_INT < 31 || + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED + fun expand(value: Boolean) { expanded = value; onExpandedChange(value) } + val labels = mapOf(MediaAudioProfile.AUTO to R.string.media_audio_auto, MediaAudioProfile.PHONE to R.string.media_audio_phone, + MediaAudioProfile.WIRED to R.string.media_audio_wired, MediaAudioProfile.BLUETOOTH to R.string.media_audio_bluetooth) + Box { + IconButton(onClick = { expand(true) }) { + Icon(if (state.active == MediaAudioProfile.PHONE) Icons.AutoMirrored.Filled.VolumeUp else Icons.Filled.HeadsetMic, + stringResource(R.string.media_audio_devices, stringResource(labels.getValue(state.preferred))), + tint = if (state.notice != null) MaterialTheme.colorScheme.error else tint) + } + DropdownMenu(expanded, onDismissRequest = { expand(false) }) { + for ((profile, label) in labels) { + val available = when (profile) { + MediaAudioProfile.WIRED -> state.wiredAvailable + MediaAudioProfile.BLUETOOTH -> state.bluetoothAvailable || !bluetoothPermission + else -> true + } + DropdownMenuItem(text = { Text(stringResource(label)) }, enabled = available, + trailingIcon = { if (state.preferred == profile) Text("✓") }, onClick = { + expand(false) + if (profile == MediaAudioProfile.BLUETOOTH && !bluetoothPermission) + permission.launch(Manifest.permission.BLUETOOTH_CONNECT) + else MediaAudioController.select(profile) + }) + } + HorizontalDivider() + DropdownMenuItem(text = { Text(stringResource(R.string.media_audio_actual, state.inputName ?: "—", state.outputName ?: "—")) }, + onClick = {}, enabled = false) + if (state.switching) DropdownMenuItem(text = { Text(stringResource(R.string.media_audio_switching)) }, onClick = {}, enabled = false) + state.notice?.let { notice -> DropdownMenuItem(text = { Text(stringResource(notice), color = MaterialTheme.colorScheme.error) }, onClick = {}) } + } + } +} diff --git a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml index 1597a647..3516ac33 100644 --- a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml @@ -582,4 +582,13 @@ Отклонять входящие звонки vibechat без рингтона Отклонять все вызовы по рации Отклонять приглашения; включённая рация продолжит работать + Аудиоустройство: %1$s + Автоматически + Телефон + Проводное / USB + Bluetooth + Микрофон: %1$s\nВыход: %2$s + Переключение аудиоустройства… + Выбранное устройство отключено. Используется микрофон и динамик телефона. + Не удалось использовать выбранное аудиоустройство. Проверьте текущие микрофон и выход. diff --git a/tools/chatgui-android/app/src/main/res/values/strings.xml b/tools/chatgui-android/app/src/main/res/values/strings.xml index f0c15dd7..d8c905dd 100644 --- a/tools/chatgui-android/app/src/main/res/values/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values/strings.xml @@ -581,4 +581,13 @@ Reject incoming vibechat calls without ringing Reject all radio calls Reject radio invitations; the channel radio keeps working + Audio device: %1$s + Automatic + Phone + Wired / USB + Bluetooth + Microphone: %1$s\nOutput: %2$s + Switching audio device… + The selected device is disconnected. Phone audio is in use. + Could not use the selected audio device. Check the current microphone and output.