mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat: redesign login screen on both app and app-tv
This commit is contained in:
@@ -18,10 +18,12 @@ 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.material3.TextButton
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||
@@ -33,12 +35,11 @@ import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
fun LoginContent(
|
||||
state: LoginContentState,
|
||||
callbacks: LoginContentCallbacks,
|
||||
isLoggingIn: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
if (isLoggingIn) {
|
||||
if (state.isLoggingIn) {
|
||||
PurefinWaitingScreen(modifier = modifier)
|
||||
return
|
||||
}
|
||||
@@ -76,21 +77,6 @@ fun LoginContent(
|
||||
|
||||
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,
|
||||
@@ -107,43 +93,177 @@ fun LoginContent(
|
||||
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)
|
||||
)
|
||||
when (state.phase) {
|
||||
LoginContentPhase.ServerSearch -> ServerSearchContent(state, callbacks)
|
||||
LoginContentPhase.Login -> LoginPhaseContent(state, callbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerSearchContent(
|
||||
state: LoginContentState,
|
||||
callbacks: LoginContentCallbacks
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Text(
|
||||
text = "Find your server",
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Search by address or choose a nearby Jellyfin server.",
|
||||
color = scheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 2.dp, bottom = 16.dp).fillMaxWidth()
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
PurefinTextButton(
|
||||
content = { Text(if (state.isSearching) "Searching..." else "Find server") },
|
||||
onClick = callbacks.onFindServer,
|
||||
enabled = !state.isSearching,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
)
|
||||
|
||||
if (state.discoveredServers.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = "Nearby servers",
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
state.discoveredServers.forEach { server ->
|
||||
TextButton(
|
||||
onClick = { callbacks.onDiscoveredServerClick(server) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = server.name?.let { "$it\n${server.address}" } ?: server.address,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoginPhaseContent(
|
||||
state: LoginContentState,
|
||||
callbacks: LoginContentCallbacks
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val selectedServer = state.selectedServerName ?: state.selectedServerUrl.orEmpty()
|
||||
|
||||
Text(
|
||||
text = "Log in",
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = selectedServer,
|
||||
color = scheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 2.dp).fillMaxWidth()
|
||||
)
|
||||
TextButton(
|
||||
onClick = callbacks.onChangeServer,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Change server")
|
||||
}
|
||||
|
||||
if (state.quickConnectAvailable) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
state.quickConnectCode?.let { code ->
|
||||
Text(
|
||||
text = code,
|
||||
color = scheme.primary,
|
||||
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Approve this code in another Jellyfin client.",
|
||||
color = scheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
TextButton(onClick = callbacks.onCancelQuickConnect) {
|
||||
Text("Cancel Quick Connect")
|
||||
}
|
||||
} ?: PurefinTextButton(
|
||||
content = { Text("Quick Connect") },
|
||||
onClick = callbacks.onQuickConnect,
|
||||
enabled = !state.isQuickConnecting,
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Quick Connect is not enabled on this server.",
|
||||
color = scheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
text = "Manual login",
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
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,
|
||||
enabled = !state.isQuickConnecting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ package hu.bbara.purefin.ui.screen.login
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.core.data.JellyfinServerCandidate
|
||||
import hu.bbara.purefin.core.feature.login.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -18,51 +17,82 @@ fun LoginScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val serverUrl by viewModel.url.collectAsState()
|
||||
val phase by viewModel.phase.collectAsState()
|
||||
val selectedServer by viewModel.selectedServer.collectAsState()
|
||||
val discoveredServers by viewModel.discoveredServers.collectAsState()
|
||||
val username by viewModel.username.collectAsState()
|
||||
val password by viewModel.password.collectAsState()
|
||||
val isSearching by viewModel.isSearching.collectAsState()
|
||||
val isLoggingIn by viewModel.isLoggingIn.collectAsState()
|
||||
val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsState()
|
||||
val quickConnectCode by viewModel.quickConnectCode.collectAsState()
|
||||
val isQuickConnecting by viewModel.isQuickConnecting.collectAsState()
|
||||
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||
var isLoggingIn by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val state = remember(serverUrl, username, password, errorMessage) {
|
||||
val state = remember(
|
||||
phase,
|
||||
serverUrl,
|
||||
selectedServer,
|
||||
discoveredServers,
|
||||
username,
|
||||
password,
|
||||
isSearching,
|
||||
isLoggingIn,
|
||||
quickConnectAvailable,
|
||||
quickConnectCode,
|
||||
isQuickConnecting,
|
||||
errorMessage
|
||||
) {
|
||||
LoginContentState(
|
||||
phase = if (phase == LoginViewModel.Phase.LOGIN) {
|
||||
LoginContentPhase.Login
|
||||
} else {
|
||||
LoginContentPhase.ServerSearch
|
||||
},
|
||||
serverUrl = serverUrl,
|
||||
selectedServerName = selectedServer?.name,
|
||||
selectedServerUrl = selectedServer?.address,
|
||||
discoveredServers = discoveredServers.map {
|
||||
LoginServerCandidateUi(name = it.name, address = it.address)
|
||||
},
|
||||
username = username,
|
||||
password = password,
|
||||
isSearching = isSearching,
|
||||
isLoggingIn = isLoggingIn,
|
||||
quickConnectAvailable = quickConnectAvailable,
|
||||
quickConnectCode = quickConnectCode,
|
||||
isQuickConnecting = isQuickConnecting,
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
}
|
||||
val callbacks = remember(viewModel, coroutineScope) {
|
||||
LoginContentCallbacks(
|
||||
onServerUrlChange = {
|
||||
viewModel.clearError()
|
||||
viewModel.setUrl(it)
|
||||
onServerUrlChange = viewModel::setUrl,
|
||||
onFindServer = viewModel::findServer,
|
||||
onDiscoveredServerClick = {
|
||||
viewModel.selectDiscoveredServer(JellyfinServerCandidate(name = it.name, address = it.address))
|
||||
},
|
||||
onChangeServer = viewModel::changeServer,
|
||||
onUsernameChange = {
|
||||
viewModel.clearError()
|
||||
viewModel.setUsername(it)
|
||||
},
|
||||
onPasswordChange = {
|
||||
viewModel.clearError()
|
||||
viewModel.setPassword(it)
|
||||
},
|
||||
onConnect = {
|
||||
coroutineScope.launch {
|
||||
isLoggingIn = true
|
||||
try {
|
||||
viewModel.login()
|
||||
} finally {
|
||||
isLoggingIn = false
|
||||
}
|
||||
viewModel.login()
|
||||
}
|
||||
}
|
||||
},
|
||||
onQuickConnect = viewModel::startQuickConnect,
|
||||
onCancelQuickConnect = viewModel::cancelQuickConnect
|
||||
)
|
||||
}
|
||||
|
||||
LoginContent(
|
||||
state = state,
|
||||
callbacks = callbacks,
|
||||
isLoggingIn = isLoggingIn,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user