From e3605175621dc67547f365679d1ea3b1ec0d0a4e Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sun, 25 Jan 2026 13:17:45 +0100 Subject: [PATCH] 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`. --- .../app/content/episode/EpisodeComponents.kt | 8 +-- .../app/content/movie/MovieComponents.kt | 8 +-- .../app/content/series/SeriesComponents.kt | 8 +-- .../hu/bbara/purefin/app/home/HomePage.kt | 1 - .../purefin/app/home/HomePageViewModel.kt | 23 +++++---- .../bbara/purefin/app/home/ui/HomeModels.kt | 6 +-- .../bbara/purefin/app/home/ui/HomeSections.kt | 6 ++- .../bbara/purefin/app/home/ui/HomeTopBar.kt | 50 +++---------------- .../purefin/app/library/LibraryViewModel.kt | 40 ++++++++++++++- .../purefin/app/library/ui/LibraryScreen.kt | 50 +++++++++++++++++-- .../common/ui/MediaDetailComponents.kt | 29 ----------- .../hu/bbara/purefin/common/ui/PosterCard.kt | 15 +++--- .../common/ui/components/GhostIconButton.kt | 40 +++++++++++++++ .../common/ui/components/PurefinAsyncImage.kt | 8 ++- .../common/ui/components/PurefinIconButton.kt | 40 +++++++++++++++ .../purefin/image/JellyfinImageHelper.kt | 3 ++ 16 files changed, 219 insertions(+), 116 deletions(-) create mode 100644 app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt create mode 100644 app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt diff --git a/app/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt b/app/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt index 1db7d8f..69149ee 100644 --- a/app/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/app/content/episode/EpisodeComponents.kt @@ -31,9 +31,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.common.ui.MediaCastMember 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.MediaSynopsis +import hu.bbara.purefin.common.ui.components.GhostIconButton import hu.bbara.purefin.common.ui.components.MediaActionButton import hu.bbara.purefin.common.ui.components.MediaPlayButton import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings @@ -52,14 +52,14 @@ internal fun EpisodeTopBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - MediaGhostIconButton( + GhostIconButton( icon = Icons.Outlined.ArrowBack, contentDescription = "Back", onClick = onBack ) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) - MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) + GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) + GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) } } } diff --git a/app/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt b/app/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt index a8686fb..304f0ee 100644 --- a/app/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/app/content/movie/MovieComponents.kt @@ -31,9 +31,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.common.ui.MediaCastMember 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.MediaSynopsis +import hu.bbara.purefin.common.ui.components.GhostIconButton import hu.bbara.purefin.common.ui.components.MediaActionButton import hu.bbara.purefin.common.ui.components.MediaPlayButton import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings @@ -53,14 +53,14 @@ internal fun MovieTopBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - MediaGhostIconButton( + GhostIconButton( icon = Icons.Outlined.ArrowBack, contentDescription = "Back", onClick = onBack ) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) - MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) + GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) + GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) } } } diff --git a/app/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt b/app/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt index b0015db..776b898 100644 --- a/app/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/app/content/series/SeriesComponents.kt @@ -46,8 +46,8 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.MediaCastMember 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.components.GhostIconButton import hu.bbara.purefin.common.ui.components.MediaActionButton import hu.bbara.purefin.common.ui.components.PurefinAsyncImage @@ -64,13 +64,13 @@ internal fun SeriesTopBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - MediaGhostIconButton( + GhostIconButton( onClick = onBack, icon = Icons.Outlined.ArrowBack, contentDescription = "Back") Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - MediaGhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) - MediaGhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) + GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) + GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More", onClick = { }) } } } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt index 3b986af..2fb1f58 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt @@ -74,7 +74,6 @@ fun HomePage( contentColor = MaterialTheme.colorScheme.onBackground, topBar = { HomeTopBar( - title = "Home", onMenuClick = { coroutineScope.launch { drawerState.open() } } ) } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt index bc50815..4c48b64 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt @@ -16,7 +16,9 @@ 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.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jellyfin.sdk.model.UUID @@ -34,7 +36,11 @@ class HomePageViewModel @Inject constructor( private val jellyfinApiClient: JellyfinApiClient ) : ViewModel() { - private val _url = MutableStateFlow("") + private val _url = userSessionRepository.serverUrl.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = "" + ) private val _continueWatching = MutableStateFlow>(emptyList()) val continueWatching = _continueWatching.asStateFlow() @@ -49,11 +55,6 @@ class HomePageViewModel @Inject constructor( val latestLibraryContent = _latestLibraryContent.asStateFlow() init { - viewModelScope.launch { - userSessionRepository.serverUrl.collect { - _url.value = it - } - } loadHomePageData() } @@ -154,7 +155,8 @@ class HomePageViewModel @Inject constructor( PosterItem( id = it.id, title = it.name ?: "Unknown", - type = it.type + type = it.type, + imageUrl = getImageUrl(it.id, ImageType.PRIMARY) ) } _libraryItems.update { currentMap -> @@ -183,19 +185,20 @@ class HomePageViewModel @Inject constructor( BaseItemKind.MOVIE -> PosterItem( id = it.id, title = it.name ?: "Unknown", - type = BaseItemKind.MOVIE + type = BaseItemKind.MOVIE, + imageUrl = getImageUrl(it.id, ImageType.PRIMARY) ) BaseItemKind.EPISODE -> PosterItem( id = it.id, title = it.seriesName ?: "Unknown", type = BaseItemKind.EPISODE, - parentId = it.seriesId!! + imageUrl = getImageUrl(it.parentId!!, ImageType.PRIMARY) ) BaseItemKind.SEASON -> PosterItem( id = it.seriesId!!, title = it.seriesName ?: "Unknown", type = BaseItemKind.SERIES, - parentId = it.seriesId + imageUrl = getImageUrl(it.id, ImageType.PRIMARY) ) else -> null } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt index a5b9c81..ecdfe73 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt @@ -26,10 +26,8 @@ data class PosterItem( val id: UUID, val title: String, val type: BaseItemKind, - val parentId: UUID? = null -) { - val imageItemId: UUID get() = parentId ?: id -} + val imageUrl: String +) data class HomeNavItem( val id: UUID, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt index f641504..e521cfb 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt @@ -161,7 +161,8 @@ fun LibraryPosterSection( title: String, items: List, action: String?, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: HomePageViewModel = hiltViewModel() ) { SectionHeader( title = title, @@ -176,6 +177,9 @@ fun LibraryPosterSection( items = items, key = { it.id }) { item -> PosterCard( item = item, + onMovieSelected = { viewModel.onMovieSelected(it) }, + onSeriesSelected = { viewModel.onSeriesSelected(it) }, + onEpisodeSelected = { viewModel.onEpisodeSelected(it) } ) } } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt index 021b36e..2ec791f 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt @@ -4,45 +4,23 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons 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.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 androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.app.home.HomePageViewModel +import hu.bbara.purefin.common.ui.components.PurefinIconButton @Composable fun HomeTopBar( - viewModel: HomePageViewModel = hiltViewModel(), - title: String, onMenuClick: () -> Unit, 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 @@ -55,34 +33,18 @@ fun HomeTopBar( Row( modifier = Modifier .statusBarsPadding() - .padding(horizontal = 12.dp, vertical = 12.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onMenuClick) { - Icon( - imageVector = Icons.Outlined.Menu, - contentDescription = "Menu", - tint = scheme.onBackground - ) - } - Text( - text = title, - color = scheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold + PurefinIconButton( + icon = Icons.Outlined.Menu, + contentDescription = "Menu", + onClick = onMenuClick ) } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Button(onClick = { viewModel.loadHomePageData() }) { - Icon(imageVector = Icons.Outlined.Refresh, contentDescription = "Refresh") - } - } } } } diff --git a/app/src/main/java/hu/bbara/purefin/app/library/LibraryViewModel.kt b/app/src/main/java/hu/bbara/purefin/app/library/LibraryViewModel.kt index 7d6751e..80fa63b 100644 --- a/app/src/main/java/hu/bbara/purefin/app/library/LibraryViewModel.kt +++ b/app/src/main/java/hu/bbara/purefin/app/library/LibraryViewModel.kt @@ -5,21 +5,50 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import hu.bbara.purefin.app.home.ui.PosterItem 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.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( + private val userSessionRepository: UserSessionRepository, private val jellyfinApiClient: JellyfinApiClient, private val navigationManager: NavigationManager ) : ViewModel() { + + private val _url = userSessionRepository.serverUrl.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = "" + ) private val _contents = MutableStateFlow>(emptyList()) 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) { viewModelScope.launch { val libraryItems = jellyfinApiClient.getLibrary(libraryId) @@ -27,9 +56,18 @@ class LibraryViewModel @Inject constructor( PosterItem( id = it.id, 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 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt b/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt index 52118d4..3c06df0 100644 --- a/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt @@ -2,11 +2,19 @@ package hu.bbara.purefin.app.library.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column 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.LazyVerticalGrid 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.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.library.LibraryViewModel import hu.bbara.purefin.common.ui.PosterCard +import hu.bbara.purefin.common.ui.components.PurefinIconButton import hu.bbara.purefin.navigation.LibraryDto @Composable @@ -30,14 +39,42 @@ fun LibraryScreen( val libraryItems = viewModel.contents.collectAsState() - - LibraryPosterGrid(libraryItems = libraryItems.value) + Scaffold( + topBar = { + LibraryTopBar( + onBack = { viewModel.onBack() } + ) + } + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + LibraryPosterGrid(libraryItems = libraryItems.value) + } + } } @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, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: LibraryViewModel = hiltViewModel() ) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 120.dp), @@ -49,7 +86,10 @@ fun LibraryPosterGrid( items(libraryItems) { item -> PosterCard( item = item, - + onMovieSelected = { viewModel.onMovieSelected(item.id.toString()) }, + onSeriesSelected = { viewModel.onSeriesSelected(item.id.toString()) }, + onEpisodeSelected = { + } ) } } diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt b/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt index 502785c..f7c4798 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt @@ -2,7 +2,6 @@ package hu.bbara.purefin.common.ui import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons 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.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -39,31 +35,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 fun MediaMetaChip( text: String, diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt b/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt index 002d06d..41d6e00 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt +++ b/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt @@ -18,26 +18,25 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp 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.common.ui.components.PurefinAsyncImage import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ImageType @Composable fun PosterCard( item: PosterItem, modifier: Modifier = Modifier, - viewModel: HomePageViewModel = hiltViewModel() + onMovieSelected: (String) -> Unit, + onSeriesSelected: (String) -> Unit, + onEpisodeSelected: (String) -> Unit, ) { val scheme = MaterialTheme.colorScheme fun openItem(posterItem: PosterItem) { when (posterItem.type) { - BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString()) - BaseItemKind.SERIES -> viewModel.onSeriesSelected(posterItem.id.toString()) - BaseItemKind.EPISODE -> viewModel.onEpisodeSelected(posterItem.id.toString()) + BaseItemKind.MOVIE -> onMovieSelected(posterItem.id.toString()) + BaseItemKind.SERIES -> onSeriesSelected(posterItem.id.toString()) + BaseItemKind.EPISODE -> onEpisodeSelected(posterItem.id.toString()) else -> {} } } @@ -46,7 +45,7 @@ fun PosterCard( .width(144.dp) ) { PurefinAsyncImage( - model = viewModel.getImageUrl(item.imageItemId, ImageType.PRIMARY), + model = item.imageUrl, contentDescription = null, modifier = Modifier .aspectRatio(2f / 3f) diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt new file mode 100644 index 0000000..64ad605 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt index 893af2a..9d510a9 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt +++ b/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt @@ -20,8 +20,14 @@ fun PurefinAsyncImage( ) { 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( - model = model, + model = effectiveModel, contentDescription = contentDescription, modifier = modifier, contentScale = contentScale, diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt new file mode 100644 index 0000000..69d74ab --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/hu/bbara/purefin/image/JellyfinImageHelper.kt b/app/src/main/java/hu/bbara/purefin/image/JellyfinImageHelper.kt index a238c46..0ccb8cd 100644 --- a/app/src/main/java/hu/bbara/purefin/image/JellyfinImageHelper.kt +++ b/app/src/main/java/hu/bbara/purefin/image/JellyfinImageHelper.kt @@ -6,6 +6,9 @@ import org.jellyfin.sdk.model.api.ImageType class JellyfinImageHelper { companion object { fun toImageUrl(url: String, itemId: UUID, type: ImageType): String { + if (url.isEmpty()) { + return "" + } return StringBuilder() .append(url) .append("/Items/")