mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
implement navigation, loading states, and series episode data
- Implement back navigation and home redirection in `EpisodeScreenViewModel`, `MovieScreenViewModel`, and `SeriesViewModel`. - Refactor `EpisodeScreen`, `MovieScreen`, and `SeriesScreen` to handle data fetching via `LaunchedEffect` and display a loading state. - Create `PurefinWaitingScreen`, a custom animated loading component with pulsing icons and dots. - Update `SeriesViewModel` to properly map episode data, including image URLs and unique identifiers. - Add click listeners to `EpisodeTopBar`, `MovieTopBar`, `SeriesTopBar`, and `EpisodeCard` to support navigation between screens. - Integrate the waiting screen into the `LoginScreen` flow during connection attempts. - Fix image URL generation in `SeriesViewModel` to use the primary image type for episodes and backdrops for series.
This commit is contained in:
@@ -12,35 +12,17 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
||||
@Composable
|
||||
fun EpisodeCard(
|
||||
item: ItemDto,
|
||||
episode: EpisodeUiModel,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: EpisodeScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
LaunchedEffect(item) {
|
||||
when (item.type) {
|
||||
BaseItemKind.EPISODE -> viewModel.selectEpisode(item.id)
|
||||
else -> return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
|
||||
val episodeItem = viewModel.episode.collectAsState()
|
||||
|
||||
if (episodeItem.value != null) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -53,7 +35,7 @@ fun EpisodeCard(
|
||||
if (isWide) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
EpisodeHero(
|
||||
episode = episodeItem.value!!,
|
||||
episode = episode,
|
||||
height = 300.dp,
|
||||
isWide = true,
|
||||
modifier = Modifier
|
||||
@@ -61,12 +43,17 @@ fun EpisodeCard(
|
||||
.weight(0.5f)
|
||||
)
|
||||
EpisodeDetails(
|
||||
episode = episodeItem.value!!,
|
||||
episode = episode,
|
||||
modifier = Modifier
|
||||
.weight(0.5f)
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = contentPadding, end = contentPadding, top = 96.dp, bottom = 32.dp)
|
||||
.padding(
|
||||
start = contentPadding,
|
||||
end = contentPadding,
|
||||
top = 96.dp,
|
||||
bottom = 32.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -76,13 +63,13 @@ fun EpisodeCard(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
EpisodeHero(
|
||||
episode = episodeItem.value!!,
|
||||
episode = episode,
|
||||
height = 400.dp,
|
||||
isWide = false,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
EpisodeDetails(
|
||||
episode = episodeItem.value!!,
|
||||
episode = episode,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = contentPadding)
|
||||
@@ -107,17 +94,5 @@ fun EpisodeCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(EpisodeBackgroundDark),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Loading...",
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,13 +59,20 @@ import coil3.compose.AsyncImage
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
|
||||
@Composable
|
||||
internal fun EpisodeTopBar(modifier: Modifier = Modifier) {
|
||||
internal fun EpisodeTopBar(
|
||||
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
GhostIconButton(icon = Icons.Outlined.ArrowBack, contentDescription = "Back")
|
||||
GhostIconButton(
|
||||
onClick = { viewModel.onBack() },
|
||||
icon = Icons.Outlined.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
||||
@@ -75,6 +82,7 @@ internal fun EpisodeTopBar(modifier: Modifier = Modifier) {
|
||||
|
||||
@Composable
|
||||
private fun GhostIconButton(
|
||||
onClick: () -> Unit = {},
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -84,7 +92,7 @@ private fun GhostIconButton(
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(EpisodeBackgroundDark.copy(alpha = 0.4f))
|
||||
.clickable { },
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
package hu.bbara.purefin.app.content.episode
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
|
||||
@Composable
|
||||
fun EpisodeScreen(
|
||||
episode: ItemDto,
|
||||
viewModel: EpisodeScreenViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
LaunchedEffect(episode) {
|
||||
viewModel.selectEpisode(episode.id)
|
||||
}
|
||||
|
||||
val episode = viewModel.episode.collectAsState()
|
||||
|
||||
if (episode.value != null) {
|
||||
EpisodeCard(
|
||||
item = episode,
|
||||
episode = episode.value!!,
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
PurefinWaitingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
import hu.bbara.purefin.navigation.NavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -12,6 +15,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.time.LocalDateTime
|
||||
@@ -23,12 +27,28 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class EpisodeScreenViewModel @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
): ViewModel() {
|
||||
|
||||
private val _episode = MutableStateFlow<EpisodeUiModel?>(null)
|
||||
val episode = _episode.asStateFlow()
|
||||
|
||||
fun onSeriesSelected(seriesId: String) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.Series(ItemDto(UUID.fromString(seriesId), BaseItemKind.SERIES)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun selectNextUpEpisodeForSeries(seriesId: UUID) {
|
||||
viewModelScope.launch {
|
||||
val episode = jellyfinApiClient.getNextUpEpisode(seriesId)
|
||||
|
||||
@@ -12,31 +12,17 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
|
||||
@Composable
|
||||
fun MovieCard(
|
||||
movie: ItemDto,
|
||||
movie: MovieUiModel,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MovieScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
LaunchedEffect(movie.id) {
|
||||
viewModel.selectMovie(movie.id)
|
||||
}
|
||||
|
||||
val movieItem = viewModel.movie.collectAsState()
|
||||
|
||||
if (movieItem.value != null) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -49,7 +35,7 @@ fun MovieCard(
|
||||
if (isWide) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
MovieHero(
|
||||
movie = movieItem.value!!,
|
||||
movie = movie,
|
||||
height = 300.dp,
|
||||
isWide = true,
|
||||
modifier = Modifier
|
||||
@@ -57,12 +43,17 @@ fun MovieCard(
|
||||
.weight(0.5f)
|
||||
)
|
||||
MovieDetails(
|
||||
movie = movieItem.value!!,
|
||||
movie = movie,
|
||||
modifier = Modifier
|
||||
.weight(0.5f)
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = contentPadding, end = contentPadding, top = 96.dp, bottom = 32.dp)
|
||||
.padding(
|
||||
start = contentPadding,
|
||||
end = contentPadding,
|
||||
top = 96.dp,
|
||||
bottom = 32.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -72,13 +63,13 @@ fun MovieCard(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MovieHero(
|
||||
movie = movieItem.value!!,
|
||||
movie = movie,
|
||||
height = 400.dp,
|
||||
isWide = false,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
MovieDetails(
|
||||
movie = movieItem.value!!,
|
||||
movie = movie,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = contentPadding)
|
||||
@@ -103,17 +94,5 @@ fun MovieCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MovieBackgroundDark),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Loading...",
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,13 +59,20 @@ import coil3.compose.AsyncImage
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
|
||||
@Composable
|
||||
internal fun MovieTopBar(modifier: Modifier = Modifier) {
|
||||
internal fun MovieTopBar(
|
||||
viewModel: MovieScreenViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
GhostIconButton(icon = Icons.Outlined.ArrowBack, contentDescription = "Back")
|
||||
GhostIconButton(
|
||||
onClick = { viewModel.onBack() },
|
||||
icon = Icons.Outlined.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
||||
@@ -77,6 +84,7 @@ internal fun MovieTopBar(modifier: Modifier = Modifier) {
|
||||
private fun GhostIconButton(
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
@@ -84,7 +92,7 @@ private fun GhostIconButton(
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MovieBackgroundDark.copy(alpha = 0.4f))
|
||||
.clickable { },
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -133,8 +141,7 @@ internal fun MovieHero(
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
MovieBackgroundDark.copy(alpha = 0.8f)
|
||||
Color.Transparent, MovieBackgroundDark.copy(alpha = 0.8f)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
package hu.bbara.purefin.app.content.movie
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
|
||||
@Composable
|
||||
fun MovieScreen(
|
||||
movie: ItemDto,
|
||||
viewModel: MovieScreenViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LaunchedEffect(movie.id) {
|
||||
viewModel.selectMovie(movie.id)
|
||||
}
|
||||
|
||||
val movieItem = viewModel.movie.collectAsState()
|
||||
|
||||
if (movieItem.value != null) {
|
||||
MovieCard(
|
||||
movie = movie,
|
||||
movie = movieItem.value!!,
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
PurefinWaitingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.navigation.NavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -20,12 +22,22 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class MovieScreenViewModel @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
): ViewModel() {
|
||||
|
||||
private val _movie = MutableStateFlow<MovieUiModel?>(null)
|
||||
val movie = _movie.asStateFlow()
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun selectMovie(movieId: UUID) {
|
||||
viewModelScope.launch {
|
||||
val movieInfo = jellyfinApiClient.getItemInfo(movieId)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -14,30 +13,20 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
|
||||
@Composable
|
||||
fun SeriesCard(
|
||||
series: ItemDto,
|
||||
series: SeriesUiModel,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel()
|
||||
) {
|
||||
LaunchedEffect(series.id) {
|
||||
viewModel.selectSeries(series.id)
|
||||
}
|
||||
|
||||
val series = viewModel.series.collectAsState()
|
||||
|
||||
if (series.value != null) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -50,25 +39,28 @@ fun SeriesCard(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SeriesHero(
|
||||
imageUrl = series.value!!.heroImageUrl,
|
||||
imageUrl = series.heroImageUrl,
|
||||
height = heroHeight
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset(y = (-96).dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = series.value!!.title,
|
||||
text = series.title,
|
||||
color = Color.White,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 36.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
SeriesMetaChips(series = series.value!!)
|
||||
SeriesMetaChips(series = series)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SeriesActionButtons()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
@@ -80,20 +72,32 @@ fun SeriesCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = series.value!!.synopsis,
|
||||
text = series.synopsis,
|
||||
color = SeriesMutedStrong,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
SeasonTabs(seasons = series.value!!.seasonTabs)
|
||||
Text(
|
||||
text = "Episodes",
|
||||
color = Color.White,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
SeasonTabs(seasons = series.seasonTabs)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
EpisodeCarousel(episodes = series.value!!.seasonTabs.firstOrNull { it.isSelected }?.episodes.orEmpty())
|
||||
|
||||
EpisodeCarousel(
|
||||
episodes = series.seasonTabs.firstOrNull { it.isSelected }?.episodes
|
||||
?: series.seasonTabs.firstOrNull()?.episodes
|
||||
?: emptyList()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp, bottom = 32.dp)
|
||||
.padding(top = 0.dp, bottom = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cast",
|
||||
@@ -103,7 +107,8 @@ fun SeriesCard(
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
CastRow(cast = series.value!!.cast)
|
||||
CastRow(cast = series.cast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,17 +119,10 @@ fun SeriesCard(
|
||||
.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(SeriesBackgroundDark),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Loading...",
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SeriesCardPreview() {
|
||||
SeriesCard(series = SeriesMockData.series())
|
||||
}
|
||||
|
||||
@@ -50,16 +50,23 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@Composable
|
||||
internal fun SeriesTopBar(modifier: Modifier = Modifier) {
|
||||
internal fun SeriesTopBar(
|
||||
viewModel: SeriesViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
GhostIconButton(icon = Icons.Outlined.ArrowBack, contentDescription = "Back")
|
||||
GhostIconButton(
|
||||
onClick = { viewModel.onBack() },
|
||||
icon = Icons.Outlined.ArrowBack,
|
||||
contentDescription = "Back")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
||||
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
||||
@@ -69,6 +76,7 @@ internal fun SeriesTopBar(modifier: Modifier = Modifier) {
|
||||
|
||||
@Composable
|
||||
private fun GhostIconButton(
|
||||
onClick: () -> Unit = {},
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -78,7 +86,7 @@ private fun GhostIconButton(
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(SeriesBackgroundDark.copy(alpha = 0.4f))
|
||||
.clickable { },
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -267,7 +275,10 @@ internal fun EpisodeCarousel(episodes: List<SeriesEpisodeUiModel>, modifier: Mod
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EpisodeCard(episode: SeriesEpisodeUiModel) {
|
||||
private fun EpisodeCard(
|
||||
viewModel: SeriesViewModel = hiltViewModel(),
|
||||
episode: SeriesEpisodeUiModel
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
@@ -275,7 +286,7 @@ private fun EpisodeCard(episode: SeriesEpisodeUiModel) {
|
||||
.background(SeriesSurfaceDark.copy(alpha = 0.3f))
|
||||
.border(1.dp, SeriesSurfaceBorder, RoundedCornerShape(16.dp))
|
||||
.padding(12.dp)
|
||||
.clickable { },
|
||||
.clickable { viewModel.onSelectEpisode(episode.id) },
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
data class SeriesEpisodeUiModel(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val duration: String,
|
||||
@@ -35,18 +36,21 @@ internal object SeriesMockData {
|
||||
fun series(): SeriesUiModel {
|
||||
val heroUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuD3hBjDpw00tDCQsK5xNcnJra301k1T4LksWVZzHieH9KHQItEQkVzhwevJvf8RkaQKdVKvObzRlfDDqa3_PNwLUlUQc1LpDih8p94VTGobEV62qi7QrmNyQm_o55KRMNWiTG3zLLpblGqo3uUNQcYmPFqfNML95dClXQ4lQNl85-zgerPPAbGPr23dswbIYCigyTAaXgrmdV_nbNQ5LdDB0Wh5cMHtP0uxz6k3ARjNom6clhphGIUF9e6YSvKuwuiZ-1lMYFg8C_4"
|
||||
val episode1 = SeriesEpisodeUiModel(
|
||||
id = "1",
|
||||
title = "E1: The Beginning",
|
||||
description = "The crew assembles for the first time as the anomaly begins to expand rapidly near Saturn's rings.",
|
||||
duration = "58m",
|
||||
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
|
||||
)
|
||||
val episode2 = SeriesEpisodeUiModel(
|
||||
id = "2",
|
||||
title = "E2: Event Horizon",
|
||||
description = "Dr. Cole discovers a frequency embedded in the rift's radiation that suggests intelligent design.",
|
||||
duration = "54m",
|
||||
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
|
||||
)
|
||||
val episode3 = SeriesEpisodeUiModel(
|
||||
id = "3",
|
||||
title = "E3: Singularity",
|
||||
description = "Tension rises as the ship approaches the event horizon, and the AI begins to behave erratically.",
|
||||
duration = "1h 02m",
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
|
||||
@Composable
|
||||
fun SeriesScreen(
|
||||
series: ItemDto,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel()
|
||||
) {
|
||||
LaunchedEffect(series.id) {
|
||||
viewModel.selectSeries(series.id)
|
||||
}
|
||||
|
||||
val series = viewModel.series.collectAsState()
|
||||
|
||||
if (series.value != null) {
|
||||
SeriesCard(
|
||||
series = series,
|
||||
series = series.value!!,
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
PurefinWaitingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.navigation.ItemDto
|
||||
import hu.bbara.purefin.navigation.NavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -12,6 +15,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import javax.inject.Inject
|
||||
@@ -19,12 +23,28 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class SeriesViewModel @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val userSessionRepository: UserSessionRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _series = MutableStateFlow<SeriesUiModel?>(null)
|
||||
val series = _series.asStateFlow()
|
||||
|
||||
fun onSelectEpisode(episodeId: String) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.Episode(ItemDto(UUID.fromString(episodeId), BaseItemKind.EPISODE)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
|
||||
fun onGoHome() {
|
||||
navigationManager.replaceAll(Route.Home)
|
||||
}
|
||||
|
||||
fun selectSeries(seriesId: UUID) {
|
||||
viewModelScope.launch {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim().ifBlank {
|
||||
@@ -35,7 +55,13 @@ class SeriesViewModel @Inject constructor(
|
||||
val episodesItemResult = seasonsItemResult.associate { season ->
|
||||
season.id to jellyfinApiClient.getEpisodesInSeason(seriesId, season.id)
|
||||
}
|
||||
_series.value = mapToSeriesUiModel(serverUrl, seriesItemResult, seasonsItemResult, episodesItemResult)
|
||||
val seriesUiModel = mapToSeriesUiModel(
|
||||
serverUrl,
|
||||
seriesItemResult,
|
||||
seasonsItemResult,
|
||||
episodesItemResult
|
||||
)
|
||||
_series.value = seriesUiModel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,25 +75,20 @@ class SeriesViewModel @Inject constructor(
|
||||
val episodeItemResult = episodesItemResult[season.id] ?: emptyList()
|
||||
val episodeItemUiModels = episodeItemResult.map { episode ->
|
||||
SeriesEpisodeUiModel(
|
||||
id = episode.id.toString(),
|
||||
title = episode.name ?: "Unknown",
|
||||
description = episode.overview ?: "",
|
||||
duration = "58m",
|
||||
imageUrl = ""
|
||||
imageUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = episode.id, type = ImageType.PRIMARY)
|
||||
)
|
||||
}
|
||||
SeriesSeasonUiModel(
|
||||
name = season.name ?: "Unknown",
|
||||
episodes = episodeItemUiModels,
|
||||
// TODO add actual logic or remove
|
||||
isSelected = false,
|
||||
)
|
||||
}
|
||||
val heroImageUrl = seriesItemResult?.let { series ->
|
||||
JellyfinImageHelper.toImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = series.id,
|
||||
type = ImageType.BACKDROP
|
||||
)
|
||||
} ?: ""
|
||||
return SeriesUiModel(
|
||||
title = seriesItemResult?.name ?: "Unknown",
|
||||
format = seriesItemResult?.container ?: "VIDEO",
|
||||
@@ -75,7 +96,11 @@ class SeriesViewModel @Inject constructor(
|
||||
year = seriesItemResult!!.productionYear?.toString() ?: seriesItemResult!!.premiereDate?.year?.toString().orEmpty(),
|
||||
seasons = "3 Seasons",
|
||||
synopsis = seriesItemResult.overview ?: "No synopsis available.",
|
||||
heroImageUrl = "",
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = seriesItemResult.id,
|
||||
type = ImageType.BACKDROP
|
||||
),
|
||||
seasonTabs = seasonUiModels,
|
||||
cast = seriesItemResult.people.orEmpty().map { it.toCastMember() }
|
||||
)
|
||||
|
||||
197
app/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt
Normal file
197
app/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt
Normal file
@@ -0,0 +1,197 @@
|
||||
package hu.bbara.purefin.common.ui
|
||||
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Movie
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun PurefinWaitingScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val accentColor = Color(0xFFBD542E)
|
||||
val backgroundColor = Color(0xFF141517)
|
||||
val surfaceColor = Color(0xFF1E2124)
|
||||
val textPrimary = Color.White
|
||||
val textSecondary = Color(0xFF9EA3A8)
|
||||
|
||||
val transition = rememberInfiniteTransition(label = "waiting-pulse")
|
||||
val pulseScale = transition.animateFloat(
|
||||
initialValue = 0.9f,
|
||||
targetValue = 1.15f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1400, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "pulse-scale"
|
||||
)
|
||||
val pulseAlpha = transition.animateFloat(
|
||||
initialValue = 0.2f,
|
||||
targetValue = 0.6f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1400, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "pulse-alpha"
|
||||
)
|
||||
|
||||
val gradient = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
accentColor.copy(alpha = 0.28f),
|
||||
backgroundColor
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(gradient)
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.background(surfaceColor.copy(alpha = 0.92f))
|
||||
.padding(horizontal = 28.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(86.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = pulseScale.value
|
||||
scaleY = pulseScale.value
|
||||
}
|
||||
.alpha(pulseAlpha.value)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = accentColor.copy(alpha = 0.6f),
|
||||
shape = RoundedCornerShape(26.dp)
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.clip(RoundedCornerShape(22.dp))
|
||||
.background(accentColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Movie,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
text = "Connecting",
|
||||
color = textPrimary,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "Summoning the media gnomes...",
|
||||
color = textSecondary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
WaitingDots(accentColor = accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaitingDots(accentColor: Color, modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "waiting-dots")
|
||||
val firstAlpha = transition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 700, delayMillis = 0, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "dot-1"
|
||||
)
|
||||
val secondAlpha = transition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 700, delayMillis = 140, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "dot-2"
|
||||
)
|
||||
val thirdAlpha = transition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 700, delayMillis = 280, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "dot-3"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
WaitingDot(alpha = firstAlpha.value, color = accentColor)
|
||||
WaitingDot(alpha = secondAlpha.value, color = accentColor)
|
||||
WaitingDot(alpha = thirdAlpha.value, color = accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaitingDot(alpha: Float, color: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.graphicsLayer {
|
||||
val scale = 0.7f + (alpha * 0.3f)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.alpha(alpha)
|
||||
.background(color, CircleShape)
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,10 @@ import androidx.compose.material3.TextButton
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -37,6 +40,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PurefinComplexTextField
|
||||
import hu.bbara.purefin.common.ui.PurefinPasswordField
|
||||
import hu.bbara.purefin.common.ui.PurefinTextButton
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.login.viewmodel.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -47,16 +51,19 @@ fun LoginScreen(
|
||||
) {
|
||||
val JellyfinOrange = Color(0xFFBD542E)
|
||||
val JellyfinBg = Color(0xFF141517)
|
||||
val JellyfinSurface = Color(0xFF1E2124)
|
||||
val TextSecondary = Color(0xFF9EA3A8)
|
||||
|
||||
// Observe ViewModel state
|
||||
val serverUrl by viewModel.url.collectAsState()
|
||||
val username by viewModel.username.collectAsState()
|
||||
val password by viewModel.password.collectAsState()
|
||||
var isLoggingIn by remember { mutableStateOf(false) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (isLoggingIn) {
|
||||
PurefinWaitingScreen(modifier = modifier)
|
||||
} else {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -149,7 +156,12 @@ fun LoginScreen(
|
||||
content = { Text("Connect") },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
isLoggingIn = true
|
||||
try {
|
||||
viewModel.login()
|
||||
} finally {
|
||||
isLoggingIn = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -173,6 +185,6 @@ fun LoginScreen(
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user