mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
refactor UI components and improve home/library screen logic
- Rename `MediaGhostIconButton` to `GhostIconButton` and move it to the common components package. - Implement `PurefinIconButton` as a new reusable UI component. - Refactor `PosterItem` to include `imageUrl`, shifting image URL generation to the ViewModel. - Update `HomePageViewModel` and `LibraryViewModel` to use `stateIn` for the server URL and handle image URL generation. - Decouple `PosterCard` from `HomePageViewModel` by passing click lambdas as parameters. - Add `LibraryTopBar` and navigation support (back button, item selection) to the `LibraryScreen`. - Enhance `PurefinAsyncImage` to handle empty string models by treating them as null to trigger placeholders. - Update `HomeTopBar` styling and replace the custom refresh button with `PurefinIconButton`.
This commit is contained in:
@@ -31,9 +31,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaCastMember
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlayButton
|
import hu.bbara.purefin.common.ui.components.MediaPlayButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||||
@@ -52,14 +52,14 @@ internal fun EpisodeTopBar(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MediaGhostIconButton(
|
GhostIconButton(
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaCastMember
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlayButton
|
import hu.bbara.purefin.common.ui.components.MediaPlayButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||||
@@ -53,14 +53,14 @@ internal fun MovieTopBar(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MediaGhostIconButton(
|
GhostIconButton(
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaCastMember
|
import hu.bbara.purefin.common.ui.MediaCastMember
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.common.ui.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaGhostIconButton
|
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||||
|
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ internal fun SeriesTopBar(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MediaGhostIconButton(
|
GhostIconButton(
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
icon = Icons.Outlined.ArrowBack,
|
icon = Icons.Outlined.ArrowBack,
|
||||||
contentDescription = "Back")
|
contentDescription = "Back")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })
|
||||||
MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ fun HomePage(
|
|||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
topBar = {
|
topBar = {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
title = "Home",
|
|
||||||
onMenuClick = { coroutineScope.launch { drawerState.open() } }
|
onMenuClick = { coroutineScope.launch { drawerState.open() } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import hu.bbara.purefin.navigation.NavigationManager
|
|||||||
import hu.bbara.purefin.navigation.Route
|
import hu.bbara.purefin.navigation.Route
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
@@ -34,7 +36,11 @@ class HomePageViewModel @Inject constructor(
|
|||||||
private val jellyfinApiClient: JellyfinApiClient
|
private val jellyfinApiClient: JellyfinApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _url = MutableStateFlow("")
|
private val _url = userSessionRepository.serverUrl.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = ""
|
||||||
|
)
|
||||||
|
|
||||||
private val _continueWatching = MutableStateFlow<List<ContinueWatchingItem>>(emptyList())
|
private val _continueWatching = MutableStateFlow<List<ContinueWatchingItem>>(emptyList())
|
||||||
val continueWatching = _continueWatching.asStateFlow()
|
val continueWatching = _continueWatching.asStateFlow()
|
||||||
@@ -49,11 +55,6 @@ class HomePageViewModel @Inject constructor(
|
|||||||
val latestLibraryContent = _latestLibraryContent.asStateFlow()
|
val latestLibraryContent = _latestLibraryContent.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
|
||||||
userSessionRepository.serverUrl.collect {
|
|
||||||
_url.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadHomePageData()
|
loadHomePageData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +155,8 @@ class HomePageViewModel @Inject constructor(
|
|||||||
PosterItem(
|
PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.name ?: "Unknown",
|
title = it.name ?: "Unknown",
|
||||||
type = it.type
|
type = it.type,
|
||||||
|
imageUrl = getImageUrl(it.id, ImageType.PRIMARY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_libraryItems.update { currentMap ->
|
_libraryItems.update { currentMap ->
|
||||||
@@ -183,19 +185,20 @@ class HomePageViewModel @Inject constructor(
|
|||||||
BaseItemKind.MOVIE -> PosterItem(
|
BaseItemKind.MOVIE -> PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.name ?: "Unknown",
|
title = it.name ?: "Unknown",
|
||||||
type = BaseItemKind.MOVIE
|
type = BaseItemKind.MOVIE,
|
||||||
|
imageUrl = getImageUrl(it.id, ImageType.PRIMARY)
|
||||||
)
|
)
|
||||||
BaseItemKind.EPISODE -> PosterItem(
|
BaseItemKind.EPISODE -> PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.seriesName ?: "Unknown",
|
title = it.seriesName ?: "Unknown",
|
||||||
type = BaseItemKind.EPISODE,
|
type = BaseItemKind.EPISODE,
|
||||||
parentId = it.seriesId!!
|
imageUrl = getImageUrl(it.parentId!!, ImageType.PRIMARY)
|
||||||
)
|
)
|
||||||
BaseItemKind.SEASON -> PosterItem(
|
BaseItemKind.SEASON -> PosterItem(
|
||||||
id = it.seriesId!!,
|
id = it.seriesId!!,
|
||||||
title = it.seriesName ?: "Unknown",
|
title = it.seriesName ?: "Unknown",
|
||||||
type = BaseItemKind.SERIES,
|
type = BaseItemKind.SERIES,
|
||||||
parentId = it.seriesId
|
imageUrl = getImageUrl(it.id, ImageType.PRIMARY)
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ data class PosterItem(
|
|||||||
val id: UUID,
|
val id: UUID,
|
||||||
val title: String,
|
val title: String,
|
||||||
val type: BaseItemKind,
|
val type: BaseItemKind,
|
||||||
val parentId: UUID? = null
|
val imageUrl: String
|
||||||
) {
|
)
|
||||||
val imageItemId: UUID get() = parentId ?: id
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HomeNavItem(
|
data class HomeNavItem(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ fun LibraryPosterSection(
|
|||||||
title: String,
|
title: String,
|
||||||
items: List<PosterItem>,
|
items: List<PosterItem>,
|
||||||
action: String?,
|
action: String?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
SectionHeader(
|
SectionHeader(
|
||||||
title = title,
|
title = title,
|
||||||
@@ -176,6 +177,9 @@ fun LibraryPosterSection(
|
|||||||
items = items, key = { it.id }) { item ->
|
items = items, key = { it.id }) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
onMovieSelected = { viewModel.onMovieSelected(it) },
|
||||||
|
onSeriesSelected = { viewModel.onSeriesSelected(it) },
|
||||||
|
onEpisodeSelected = { viewModel.onEpisodeSelected(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,45 +4,23 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Menu
|
import androidx.compose.material.icons.outlined.Menu
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||||
import hu.bbara.purefin.app.home.HomePageViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(
|
fun HomeTopBar(
|
||||||
viewModel: HomePageViewModel = hiltViewModel(),
|
|
||||||
title: String,
|
|
||||||
onMenuClick: () -> Unit,
|
onMenuClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
actions: @Composable RowScope.() -> Unit = {
|
|
||||||
HomeAvatar(
|
|
||||||
size = 36.dp,
|
|
||||||
borderWidth = 2.dp,
|
|
||||||
borderColor = MaterialTheme.colorScheme.outline,
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
icon = Icons.Outlined.Person,
|
|
||||||
iconTint = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
@@ -55,34 +33,18 @@ fun HomeTopBar(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
IconButton(onClick = onMenuClick) {
|
PurefinIconButton(
|
||||||
Icon(
|
icon = Icons.Outlined.Menu,
|
||||||
imageVector = Icons.Outlined.Menu,
|
contentDescription = "Menu",
|
||||||
contentDescription = "Menu",
|
onClick = onMenuClick
|
||||||
tint = scheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
Button(onClick = { viewModel.loadHomePageData() }) {
|
|
||||||
Icon(imageVector = Icons.Outlined.Refresh, contentDescription = "Refresh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,50 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.app.home.ui.PosterItem
|
import hu.bbara.purefin.app.home.ui.PosterItem
|
||||||
import hu.bbara.purefin.client.JellyfinApiClient
|
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.NavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.Route
|
||||||
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryViewModel @Inject constructor(
|
class LibraryViewModel @Inject constructor(
|
||||||
|
private val userSessionRepository: UserSessionRepository,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val navigationManager: NavigationManager
|
private val navigationManager: NavigationManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _url = userSessionRepository.serverUrl.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = ""
|
||||||
|
)
|
||||||
private val _contents = MutableStateFlow<List<PosterItem>>(emptyList())
|
private val _contents = MutableStateFlow<List<PosterItem>>(emptyList())
|
||||||
val contents = _contents.asStateFlow()
|
val contents = _contents.asStateFlow()
|
||||||
|
|
||||||
|
fun onMovieSelected(movieId: String) {
|
||||||
|
navigationManager.navigate(Route.Movie(ItemDto(id = UUID.fromString(movieId), type = BaseItemKind.MOVIE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSeriesSelected(seriesId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
navigationManager.navigate(Route.Series(ItemDto(id = UUID.fromString(seriesId), type = BaseItemKind.SERIES)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBack() {
|
||||||
|
navigationManager.pop()
|
||||||
|
}
|
||||||
|
|
||||||
fun selectLibrary(libraryId: UUID) {
|
fun selectLibrary(libraryId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val libraryItems = jellyfinApiClient.getLibrary(libraryId)
|
val libraryItems = jellyfinApiClient.getLibrary(libraryId)
|
||||||
@@ -27,9 +56,18 @@ class LibraryViewModel @Inject constructor(
|
|||||||
PosterItem(
|
PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.name ?: "Unknown",
|
title = it.name ?: "Unknown",
|
||||||
type = it.type
|
type = it.type,
|
||||||
|
imageUrl = getImageUrl(it.id, ImageType.PRIMARY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getImageUrl(itemId: UUID, type: ImageType): String {
|
||||||
|
return JellyfinImageHelper.toImageUrl(
|
||||||
|
url = _url.value,
|
||||||
|
itemId = itemId,
|
||||||
|
type = type
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,19 @@ package hu.bbara.purefin.app.library.ui
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -16,6 +24,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import hu.bbara.purefin.app.home.ui.PosterItem
|
import hu.bbara.purefin.app.home.ui.PosterItem
|
||||||
import hu.bbara.purefin.app.library.LibraryViewModel
|
import hu.bbara.purefin.app.library.LibraryViewModel
|
||||||
import hu.bbara.purefin.common.ui.PosterCard
|
import hu.bbara.purefin.common.ui.PosterCard
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||||
import hu.bbara.purefin.navigation.LibraryDto
|
import hu.bbara.purefin.navigation.LibraryDto
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -30,14 +39,42 @@ fun LibraryScreen(
|
|||||||
|
|
||||||
val libraryItems = viewModel.contents.collectAsState()
|
val libraryItems = viewModel.contents.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
LibraryPosterGrid(libraryItems = libraryItems.value)
|
topBar = {
|
||||||
|
LibraryTopBar(
|
||||||
|
onBack = { viewModel.onBack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(modifier = Modifier.padding(innerPadding)) {
|
||||||
|
LibraryPosterGrid(libraryItems = libraryItems.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryPosterGrid(
|
internal fun LibraryTopBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
PurefinIconButton(
|
||||||
|
icon = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
onClick = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LibraryPosterGrid(
|
||||||
libraryItems: List<PosterItem>,
|
libraryItems: List<PosterItem>,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: LibraryViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||||
@@ -49,7 +86,10 @@ fun LibraryPosterGrid(
|
|||||||
items(libraryItems) { item ->
|
items(libraryItems) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
onMovieSelected = { viewModel.onMovieSelected(item.id.toString()) },
|
||||||
|
onSeriesSelected = { viewModel.onSeriesSelected(item.id.toString()) },
|
||||||
|
onEpisodeSelected = {
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package hu.bbara.purefin.common.ui
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -12,12 +11,10 @@ import androidx.compose.foundation.layout.aspectRatio
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Person
|
import androidx.compose.material.icons.outlined.Person
|
||||||
@@ -29,7 +26,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -39,31 +35,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaGhostIconButton(
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(scheme.background.copy(alpha = 0.4f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = scheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaMetaChip(
|
fun MediaMetaChip(
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -18,26 +18,25 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import hu.bbara.purefin.app.home.HomePageViewModel
|
|
||||||
import hu.bbara.purefin.app.home.ui.PosterItem
|
import hu.bbara.purefin.app.home.ui.PosterItem
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: PosterItem,
|
item: PosterItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: HomePageViewModel = hiltViewModel()
|
onMovieSelected: (String) -> Unit,
|
||||||
|
onSeriesSelected: (String) -> Unit,
|
||||||
|
onEpisodeSelected: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
fun openItem(posterItem: PosterItem) {
|
fun openItem(posterItem: PosterItem) {
|
||||||
when (posterItem.type) {
|
when (posterItem.type) {
|
||||||
BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString())
|
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id.toString())
|
||||||
BaseItemKind.SERIES -> viewModel.onSeriesSelected(posterItem.id.toString())
|
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id.toString())
|
||||||
BaseItemKind.EPISODE -> viewModel.onEpisodeSelected(posterItem.id.toString())
|
BaseItemKind.EPISODE -> onEpisodeSelected(posterItem.id.toString())
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +45,7 @@ fun PosterCard(
|
|||||||
.width(144.dp)
|
.width(144.dp)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = viewModel.getImageUrl(item.imageItemId, ImageType.PRIMARY),
|
model = item.imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.aspectRatio(2f / 3f)
|
.aspectRatio(2f / 3f)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GhostIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(scheme.background.copy(alpha = 0.65f))
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = scheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,8 +20,14 @@ fun PurefinAsyncImage(
|
|||||||
) {
|
) {
|
||||||
val placeholderPainter = ColorPainter(MaterialTheme.colorScheme.surfaceVariant)
|
val placeholderPainter = ColorPainter(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
|
// Convert empty string to null to properly trigger fallback
|
||||||
|
val effectiveModel = when {
|
||||||
|
model is String && model.isEmpty() -> null
|
||||||
|
else -> model
|
||||||
|
}
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = model,
|
model = effectiveModel,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(scheme.secondary)
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = scheme.onSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ import org.jellyfin.sdk.model.api.ImageType
|
|||||||
class JellyfinImageHelper {
|
class JellyfinImageHelper {
|
||||||
companion object {
|
companion object {
|
||||||
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
|
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return StringBuilder()
|
return StringBuilder()
|
||||||
.append(url)
|
.append(url)
|
||||||
.append("/Items/")
|
.append("/Items/")
|
||||||
|
|||||||
Reference in New Issue
Block a user