mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: redesign login screen on both app and app-tv
This commit is contained in:
@@ -59,7 +59,7 @@ fun LoginScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginContent(
|
TvLoginContent(
|
||||||
state = state,
|
state = state,
|
||||||
callbacks = callbacks,
|
callbacks = callbacks,
|
||||||
isLoggingIn = isLoggingIn,
|
isLoggingIn = isLoggingIn,
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.login
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.tv.material3.Button
|
||||||
|
import androidx.tv.material3.MaterialTheme as TvMaterialTheme
|
||||||
|
import androidx.tv.material3.Text as TvText
|
||||||
|
import androidx.tv.material3.darkColorScheme
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||||
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvLoginContent(
|
||||||
|
state: LoginContentState,
|
||||||
|
callbacks: LoginContentCallbacks,
|
||||||
|
isLoggingIn: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (isLoggingIn) {
|
||||||
|
PurefinWaitingScreen(modifier = modifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ProvideTvLoginTheme {
|
||||||
|
TvLoginForm(
|
||||||
|
state = state,
|
||||||
|
callbacks = callbacks,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProvideTvLoginTheme(content: @Composable () -> Unit) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
TvMaterialTheme(
|
||||||
|
colorScheme = darkColorScheme(
|
||||||
|
primary = scheme.primary,
|
||||||
|
onPrimary = scheme.onPrimary,
|
||||||
|
primaryContainer = scheme.primaryContainer,
|
||||||
|
onPrimaryContainer = scheme.onPrimaryContainer,
|
||||||
|
secondary = scheme.secondary,
|
||||||
|
onSecondary = scheme.onSecondary,
|
||||||
|
secondaryContainer = scheme.secondaryContainer,
|
||||||
|
onSecondaryContainer = scheme.onSecondaryContainer,
|
||||||
|
tertiary = scheme.tertiary,
|
||||||
|
onTertiary = scheme.onTertiary,
|
||||||
|
tertiaryContainer = scheme.tertiaryContainer,
|
||||||
|
onTertiaryContainer = scheme.onTertiaryContainer,
|
||||||
|
background = scheme.background,
|
||||||
|
onBackground = scheme.onBackground,
|
||||||
|
surface = scheme.surface,
|
||||||
|
onSurface = scheme.onSurface,
|
||||||
|
surfaceVariant = scheme.surfaceVariant,
|
||||||
|
onSurfaceVariant = scheme.onSurfaceVariant,
|
||||||
|
surfaceTint = scheme.surfaceTint,
|
||||||
|
inverseSurface = scheme.inverseSurface,
|
||||||
|
inverseOnSurface = scheme.inverseOnSurface,
|
||||||
|
error = scheme.error,
|
||||||
|
onError = scheme.onError,
|
||||||
|
errorContainer = scheme.errorContainer,
|
||||||
|
onErrorContainer = scheme.onErrorContainer,
|
||||||
|
border = scheme.outline,
|
||||||
|
borderVariant = scheme.outlineVariant,
|
||||||
|
scrim = scheme.scrim
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TvLoginForm(
|
||||||
|
state: LoginContentState,
|
||||||
|
callbacks: LoginContentCallbacks,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val serverFocusRequester = remember { FocusRequester() }
|
||||||
|
val usernameFocusRequester = remember { FocusRequester() }
|
||||||
|
val passwordFocusRequester = remember { FocusRequester() }
|
||||||
|
val connectFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
serverFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(scheme.background)
|
||||||
|
.padding(horizontal = 64.dp, vertical = 32.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 480.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
PurefinLogo(
|
||||||
|
color = scheme.primary,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Purefin",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Connect to your media server",
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
state.errorMessage?.let { errorMessage ->
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = scheme.onErrorContainer,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = scheme.errorContainer,
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
TvLoginTextField(
|
||||||
|
label = "Server URL",
|
||||||
|
value = state.serverUrl,
|
||||||
|
onValueChange = callbacks.onServerUrlChange,
|
||||||
|
placeholder = "http://192.168.1.100:8096",
|
||||||
|
leadingIcon = Icons.Default.Storage,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(serverFocusRequester)
|
||||||
|
.focusProperties {
|
||||||
|
down = usernameFocusRequester
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
TvLoginTextField(
|
||||||
|
label = "Username",
|
||||||
|
value = state.username,
|
||||||
|
onValueChange = callbacks.onUsernameChange,
|
||||||
|
placeholder = "Enter your username",
|
||||||
|
leadingIcon = Icons.Default.Person,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(usernameFocusRequester)
|
||||||
|
.focusProperties {
|
||||||
|
up = serverFocusRequester
|
||||||
|
down = passwordFocusRequester
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
TvLoginPasswordField(
|
||||||
|
label = "Password",
|
||||||
|
value = state.password,
|
||||||
|
onValueChange = callbacks.onPasswordChange,
|
||||||
|
placeholder = "Enter your password",
|
||||||
|
leadingIcon = Icons.Default.Lock,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(passwordFocusRequester)
|
||||||
|
.focusProperties {
|
||||||
|
up = usernameFocusRequester
|
||||||
|
down = connectFocusRequester
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(22.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = callbacks.onConnect,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(connectFocusRequester)
|
||||||
|
.focusProperties {
|
||||||
|
up = passwordFocusRequester
|
||||||
|
}
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
) {
|
||||||
|
TvText(
|
||||||
|
text = "Connect",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TvLoginTextField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
leadingIcon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
trailingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val shape = RoundedCornerShape(14.dp)
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
modifier = Modifier.padding(bottom = 6.dp)
|
||||||
|
)
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.onFocusChanged { isFocused = it.isFocused || it.hasFocus }
|
||||||
|
.border(
|
||||||
|
width = if (isFocused) 2.dp else 1.dp,
|
||||||
|
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.4f),
|
||||||
|
shape = shape
|
||||||
|
)
|
||||||
|
.clip(shape),
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = leadingIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = scheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = trailingIcon,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = scheme.surfaceContainer,
|
||||||
|
unfocusedContainerColor = scheme.surface,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = scheme.primary,
|
||||||
|
focusedTextColor = scheme.onSurface,
|
||||||
|
unfocusedTextColor = scheme.onSurface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TvLoginPasswordField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
leadingIcon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
var isPasswordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
TvLoginTextField(
|
||||||
|
label = label,
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
placeholder = placeholder,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
modifier = modifier,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isPasswordVisible) {
|
||||||
|
Icons.Default.VisibilityOff
|
||||||
|
} else {
|
||||||
|
Icons.Default.Visibility
|
||||||
|
},
|
||||||
|
contentDescription = if (isPasswordVisible) "Hide password" else "Show password",
|
||||||
|
tint = scheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualTransformation = if (isPasswordVisible) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.login
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||||
|
import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
|
||||||
|
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
|
||||||
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginContent(
|
||||||
|
state: LoginContentState,
|
||||||
|
callbacks: LoginContentCallbacks,
|
||||||
|
isLoggingIn: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
if (isLoggingIn) {
|
||||||
|
PurefinWaitingScreen(modifier = modifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(scheme.background)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 420.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
PurefinLogo(
|
||||||
|
color = scheme.primary,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Purefin",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Personal media, your way",
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Connect to server",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
modifier = Modifier.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Enter your server and account details.",
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(top = 2.dp, bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
state.errorMessage?.let { errorMessage ->
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = scheme.onErrorContainer,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
scheme.errorContainer,
|
||||||
|
RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
PurefinComplexTextField(
|
||||||
|
label = "Server URL",
|
||||||
|
value = state.serverUrl,
|
||||||
|
onValueChange = callbacks.onServerUrlChange,
|
||||||
|
placeholder = "http://192.168.1.100:8096",
|
||||||
|
leadingIcon = Icons.Default.Storage
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
PurefinComplexTextField(
|
||||||
|
label = "Username",
|
||||||
|
value = state.username,
|
||||||
|
onValueChange = callbacks.onUsernameChange,
|
||||||
|
placeholder = "Enter your username",
|
||||||
|
leadingIcon = Icons.Default.Person
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
PurefinPasswordField(
|
||||||
|
label = "Password",
|
||||||
|
value = state.password,
|
||||||
|
onValueChange = callbacks.onPasswordChange,
|
||||||
|
placeholder = "Enter your password",
|
||||||
|
leadingIcon = Icons.Default.Lock,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
PurefinTextButton(
|
||||||
|
content = { Text("Connect") },
|
||||||
|
onClick = callbacks.onConnect,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import hu.bbara.purefin.R
|
import hu.bbara.purefin.core.ui.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PurefinLogo(
|
fun PurefinLogo(
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.screen.login
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Lock
|
|
||||||
import androidx.compose.material.icons.filled.Movie
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material.icons.filled.Storage
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
|
||||||
import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
|
|
||||||
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
|
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class LoginContentState(
|
|
||||||
val serverUrl: String,
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
val errorMessage: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class LoginContentCallbacks(
|
|
||||||
val onServerUrlChange: (String) -> Unit,
|
|
||||||
val onUsernameChange: (String) -> Unit,
|
|
||||||
val onPasswordChange: (String) -> Unit,
|
|
||||||
val onConnect: () -> Unit,
|
|
||||||
val onDiscoverServers: () -> Unit = {},
|
|
||||||
val onNeedHelp: () -> Unit = {}
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoginContent(
|
|
||||||
state: LoginContentState,
|
|
||||||
callbacks: LoginContentCallbacks,
|
|
||||||
isLoggingIn: Boolean,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
if (isLoggingIn) {
|
|
||||||
PurefinWaitingScreen(modifier = modifier)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(scheme.background)
|
|
||||||
.padding(24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.weight(0.5f))
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(100.dp)
|
|
||||||
.background(scheme.primary, RoundedCornerShape(24.dp)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Movie,
|
|
||||||
contentDescription = "Logo",
|
|
||||||
tint = scheme.onPrimary,
|
|
||||||
modifier = Modifier.size(60.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Jellyfin",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(top = 16.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "PERSONAL MEDIA SYSTEM",
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
letterSpacing = 2.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Connect to Server",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.align(Alignment.Start)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Enter your details to access your library",
|
|
||||||
color = scheme.onSurfaceVariant,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Start)
|
|
||||||
.padding(bottom = 24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
state.errorMessage?.let { errorMessage ->
|
|
||||||
Text(
|
|
||||||
text = errorMessage,
|
|
||||||
color = scheme.error,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
scheme.errorContainer,
|
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(12.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
PurefinComplexTextField(
|
|
||||||
label = "Server URL",
|
|
||||||
value = state.serverUrl,
|
|
||||||
onValueChange = callbacks.onServerUrlChange,
|
|
||||||
placeholder = "http://192.168.1.100:8096",
|
|
||||||
leadingIcon = Icons.Default.Storage
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
PurefinComplexTextField(
|
|
||||||
label = "Username",
|
|
||||||
value = state.username,
|
|
||||||
onValueChange = callbacks.onUsernameChange,
|
|
||||||
placeholder = "Enter your username",
|
|
||||||
leadingIcon = Icons.Default.Person
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
PurefinPasswordField(
|
|
||||||
label = "Password",
|
|
||||||
value = state.password,
|
|
||||||
onValueChange = callbacks.onPasswordChange,
|
|
||||||
placeholder = "••••••••",
|
|
||||||
leadingIcon = Icons.Default.Lock,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
PurefinTextButton(
|
|
||||||
content = { Text("Connect") },
|
|
||||||
onClick = callbacks.onConnect
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(0.5f))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
TextButton(onClick = callbacks.onDiscoverServers) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = scheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Text(" Discover Servers", color = scheme.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TextButton(onClick = callbacks.onNeedHelp) {
|
|
||||||
Text("Need Help?", color = scheme.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.login
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class LoginContentState(
|
||||||
|
val serverUrl: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class LoginContentCallbacks(
|
||||||
|
val onServerUrlChange: (String) -> Unit,
|
||||||
|
val onUsernameChange: (String) -> Unit,
|
||||||
|
val onPasswordChange: (String) -> Unit,
|
||||||
|
val onConnect: () -> Unit
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user