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 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 = { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ fun HomePage(
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
topBar = {
|
||||
HomeTopBar(
|
||||
title = "Home",
|
||||
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.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<List<ContinueWatchingItem>>(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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -161,7 +161,8 @@ fun LibraryPosterSection(
|
||||
title: String,
|
||||
items: List<PosterItem>,
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<PosterItem>>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<PosterItem>,
|
||||
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 = {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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 {
|
||||
companion object {
|
||||
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(url)
|
||||
.append("/Items/")
|
||||
|
||||
Reference in New Issue
Block a user