package ru.stenfredd.movies.ui import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.remember import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.tv.material3.Button import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import ru.stenfredd.movies.core.jellyfin.JellyfinSession import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor(val session: JellyfinSession) : ViewModel() @Composable fun LoginScreen(onLoggedIn: () -> Unit, vm: LoginViewModel = hiltViewModel()) { var url by rememberSaveable { mutableStateOf("https://") } var user by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } var error by rememberSaveable { mutableStateOf(null) } var busy by rememberSaveable { mutableStateOf(false) } val scope = rememberCoroutineScope() Column( modifier = Modifier.fillMaxSize().padding(48.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text("Вход на сервер Jellyfin", style = MaterialTheme.typography.headlineMedium) Field("Адрес сервера", url, KeyboardType.Uri) { url = it } Field("Пользователь", user, KeyboardType.Text) { user = it } Field("Пароль", password, KeyboardType.Password, PasswordVisualTransformation()) { password = it } Button(enabled = !busy, onClick = { busy = true error = null scope.launch { runCatching { vm.session.login(url, user, password) } .onSuccess { onLoggedIn() } .onFailure { error = it.message ?: it.toString() } busy = false } }) { Text(if (busy) "Вход…" else "Войти") } error?.let { Text(it, color = MaterialTheme.colorScheme.error) } } } @Composable fun Field( label: String, value: String, type: KeyboardType, transformation: VisualTransformation = VisualTransformation.None, onChange: (String) -> Unit, ) { val focusManager = LocalFocusManager.current var focused by remember { mutableStateOf(false) } Column(Modifier.width(560.dp)) { Text(label, style = MaterialTheme.typography.labelLarge) BasicTextField( value = value, onValueChange = onChange, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = type, imeAction = ImeAction.Next), keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), visualTransformation = transformation, textStyle = TextStyle(color = Color.White, fontSize = 20.sp), cursorBrush = SolidColor(Color.White), modifier = Modifier .width(560.dp) .onFocusChanged { focused = it.isFocused } // A text field swallows D-pad keys; on a remote up/down must still move between fields. .onPreviewKeyEvent { e -> if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false when (e.key) { Key.DirectionDown -> focusManager.moveFocus(FocusDirection.Down) Key.DirectionUp -> focusManager.moveFocus(FocusDirection.Up) else -> false } } .border(if (focused) 3.dp else 1.dp, if (focused) Color.White else Color.Gray) .padding(12.dp), ) } }