Added logic for Searching through available medias and basic ui

This commit is contained in:
2026-03-26 22:22:56 +01:00
parent f91c2f88a1
commit 9eccf859bc
7 changed files with 220 additions and 19 deletions

View File

@@ -20,7 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.feature.shared.home.HomePageViewModel import hu.bbara.purefin.feature.shared.home.AppViewModel
import hu.bbara.purefin.tv.home.ui.TvHomeContent import hu.bbara.purefin.tv.home.ui.TvHomeContent
import hu.bbara.purefin.tv.home.ui.TvHomeDrawerContent import hu.bbara.purefin.tv.home.ui.TvHomeDrawerContent
import hu.bbara.purefin.tv.home.ui.TvHomeMockData import hu.bbara.purefin.tv.home.ui.TvHomeMockData
@@ -31,7 +31,7 @@ import org.jellyfin.sdk.model.api.CollectionType
@Composable @Composable
fun TvHomePage( fun TvHomePage(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: AppViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)

View File

@@ -11,11 +11,11 @@ import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.app.home.ui.HomeNavItem import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.feature.shared.home.HomePageViewModel import hu.bbara.purefin.feature.shared.home.AppViewModel
@Composable @Composable
fun AppScreen( fun AppScreen(
viewModel: HomePageViewModel = hiltViewModel(), viewModel: AppViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }

View File

@@ -7,23 +7,24 @@ import androidx.compose.foundation.layout.Row
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.outlined.Cloud
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import hu.bbara.purefin.common.ui.components.PurefinIconButton import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.components.SearchField import hu.bbara.purefin.common.ui.components.PurefinSearchBar
import hu.bbara.purefin.feature.shared.search.SearchViewModel
@Composable @Composable
fun HomeTopBar( fun HomeTopBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
searchViewModel: SearchViewModel = hiltViewModel()
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val searchResult = searchViewModel.searchResult.collectAsState()
Box( Box(
modifier = modifier modifier = modifier
@@ -37,15 +38,16 @@ fun HomeTopBar(
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) { ) {
SearchField( PurefinSearchBar(
value = "", onQueryChange = {
onValueChange = {}, searchViewModel.search(it)
placeholder = "Search", },
backgroundColor = scheme.secondaryContainer, onSearch = {
textColor = scheme.onSecondaryContainer, searchViewModel.search(it)
cursorColor = scheme.onSecondaryContainer, },
searchResults = searchResult.value,
modifier = Modifier.weight(1.0f, true), modifier = Modifier.weight(1.0f, true),
) )
} }

View File

@@ -0,0 +1,106 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.SearchResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PurefinSearchBar(
onQueryChange: (String) -> Unit,
onSearch: (String) -> Unit,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier
) {
var query by remember { mutableStateOf("") }
var expanded by rememberSaveable { mutableStateOf(false) }
Box(
modifier
.fillMaxWidth()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
query = query,
onQueryChange = {
query = it
onQueryChange(it)
},
onSearch = {
onSearch(query)
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 120.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.background(MaterialTheme.colorScheme.background)
) {
items(searchResults) { item ->
SearchResultCard(item)
}
}
}
}
}
@Composable
private fun SearchResultCard(
item: SearchResult,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
PurefinAsyncImage(
model = item.posterUrl,
contentDescription = item.title,
modifier = Modifier.height(150.dp),
contentScale = ContentScale.Fit
)
Text(
text = item.title,
modifier = Modifier.padding(horizontal = 8.dp)
)
}
}

View File

@@ -0,0 +1,31 @@
package hu.bbara.purefin.core.model
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
data class SearchResult(
val id: UUID,
val title: String,
val posterUrl: String,
val type: BaseItemKind,
) {
companion object {
fun create(movie: Movie, imageUrl: String) : SearchResult {
return SearchResult(
id = movie.id,
title = movie.title,
posterUrl = imageUrl,
type = BaseItemKind.MOVIE
)
}
fun create(series: Series, imageUrl: String) : SearchResult {
return SearchResult(
id = series.id,
title = series.name,
posterUrl = imageUrl,
type = BaseItemKind.MOVIE
)
}
}
}

View File

@@ -12,8 +12,8 @@ import hu.bbara.purefin.core.data.navigation.NavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.data.navigation.SeriesDto
import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.feature.download.MediaDownloadManager
import hu.bbara.purefin.core.model.Media import hu.bbara.purefin.core.model.Media
import hu.bbara.purefin.feature.download.MediaDownloadManager
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -28,7 +28,7 @@ import org.jellyfin.sdk.model.api.CollectionType
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomePageViewModel @Inject constructor( class AppViewModel @Inject constructor(
private val appContentRepository: AppContentRepository, private val appContentRepository: AppContentRepository,
private val userSessionRepository: UserSessionRepository, private val userSessionRepository: UserSessionRepository,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,

View File

@@ -0,0 +1,62 @@
package hu.bbara.purefin.feature.shared.search
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.data.MediaRepository
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.SearchResult
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@OptIn(FlowPreview::class)
class SearchViewModel @Inject constructor(
private val mediaRepository: MediaRepository,
private val userSessionRepository: UserSessionRepository
) : ViewModel() {
private val _searchResult = MutableStateFlow<List<SearchResult>>(emptyList())
val searchResult = _searchResult.asStateFlow()
private val query = MutableStateFlow("")
init {
combine(
query.debounce(300).distinctUntilChanged(),
mediaRepository.movies,
mediaRepository.series
) { currentQuery, movies, series ->
val filteredMovies = movies.filter {
it.value.title.contains(currentQuery, ignoreCase = true)
}
val filteredSeries = series.filter {
it.value.name.contains(currentQuery, ignoreCase = true)
}
_searchResult.value = filteredMovies.values.map {
SearchResult.create(it, createImageUrl(it.id))
} + filteredSeries.values.map {
SearchResult.create(it, createImageUrl(it.id))
}
}.launchIn(viewModelScope)
}
fun search(query: String) {
this.query.value = query
}
private suspend fun createImageUrl(id: UUID) : String {
return JellyfinImageHelper.toImageUrl(userSessionRepository.serverUrl.first(), id,
ImageType.PRIMARY)
}
}