mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Added logic for Searching through available medias and basic ui
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user