121 lines
5.2 KiB
Kotlin
121 lines
5.2 KiB
Kotlin
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<String?>(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),
|
|
)
|
|
}
|
|
}
|