PoC UI for search & discover

This commit is contained in:
Damontecres 2025-12-07 20:25:24 -05:00
parent d08bd40a68
commit 0281d00907
No known key found for this signature in database
6 changed files with 109 additions and 17 deletions

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.api.seerr
import com.github.damontecres.wholphin.api.seerr.infrastructure.ApiClient
import okhttp3.Call
import okhttp3.OkHttpClient
import timber.log.Timber
class SeerrApiClient(
val baseUrl: String,
@ -13,6 +14,7 @@ class SeerrApiClient(
okHttpClient
.newBuilder()
.addInterceptor {
Timber.d("SeerrApiClient: ${it.request().method} ${it.request().url}")
it.proceed(
it
.request()

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.services
import android.content.Context
import android.content.SharedPreferences
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
@ -21,6 +22,8 @@ class SeerrApi(
)
private set
val active: Boolean get() = api.baseUrl.isNotNullOrBlank() && api.apiKey.isNotNullOrBlank()
fun update(
baseUrl: String,
apiKey: String,

View file

@ -5,7 +5,7 @@ import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResul
import javax.inject.Inject
import javax.inject.Singleton
typealias SearchResult = SearchGet200ResponseResultsInner
typealias SeerrSearchResult = SearchGet200ResponseResultsInner
@Singleton
class SeerrService
@ -15,6 +15,8 @@ class SeerrService
) {
private val api: SeerrApiClient get() = seerApi.api
val active: Boolean get() = seerApi.active
suspend fun init() {
api.searchApi.discoverTvGet(1)
}
@ -22,7 +24,7 @@ class SeerrService
suspend fun search(
query: String,
page: Int = 1,
): List<SearchResult> =
): List<SeerrSearchResult> =
api.searchApi
.searchGet(query = query, page = page)
.results

View file

@ -42,6 +42,8 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrSearchResult
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.EpisodeCard
@ -51,7 +53,9 @@ import com.github.damontecres.wholphin.ui.components.SearchEditTextBox
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -73,11 +77,13 @@ class SearchViewModel
constructor(
val api: ApiClient,
val navigationManager: NavigationManager,
private val seerrService: SeerrService,
) : ViewModel() {
val movies = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val series = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val episodes = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val collections = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val seerrResults = MutableLiveData<SearchResult>(SearchResult.NoQuery)
private var currentQuery: String? = null
@ -95,11 +101,13 @@ class SearchViewModel
searchInternal(query, BaseItemKind.SERIES, series)
searchInternal(query, BaseItemKind.EPISODE, episodes)
searchInternal(query, BaseItemKind.BOX_SET, collections)
searchSeerr(query)
} else {
movies.value = SearchResult.NoQuery
series.value = SearchResult.NoQuery
episodes.value = SearchResult.NoQuery
collections.value = SearchResult.NoQuery
seerrResults.value = SearchResult.NoQuery
}
}
@ -133,6 +141,16 @@ class SearchViewModel
}
}
private fun searchSeerr(query: String) {
if (seerrService.active) {
viewModelScope.launchIO {
seerrResults.setValueOnMain(SearchResult.Searching)
val results = seerrService.search(query)
seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results))
}
}
}
fun getHints(query: String) {
// TODO
// api.searchApi.getSearchHints()
@ -151,12 +169,17 @@ sealed interface SearchResult {
data class Success(
val items: List<BaseItem?>,
) : SearchResult
data class SuccessSeerr(
val items: List<SeerrSearchResult>,
) : SearchResult
}
private const val MOVIE_ROW = 0
private const val COLLECTION_ROW = MOVIE_ROW + 1
private const val SERIES_ROW = COLLECTION_ROW + 1
private const val EPISODE_ROW = SERIES_ROW + 1
private const val SEERR_ROW = EPISODE_ROW + 1
@Composable
fun SearchPage(
@ -171,6 +194,7 @@ fun SearchPage(
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
val seerrResults by viewModel.seerrResults.observeAsState(SearchResult.NoQuery)
val query = rememberTextFieldState()
val focusRequester = remember { FocusRequester() }
@ -287,6 +311,18 @@ fun SearchPage(
)
},
)
searchResultRow(
title = context.getString(R.string.discover),
result = seerrResults,
rowIndex = SEERR_ROW,
position = position,
focusRequester = focusRequester,
onClickItem = { index, item ->
// TODO seerr
},
onClickPosition = { position = it },
modifier = Modifier.fillMaxWidth(),
)
}
}
@ -364,6 +400,40 @@ fun LazyListScope.searchResultRow(
)
}
}
is SearchResult.SuccessSeerr -> {
if (r.items.isEmpty()) {
SearchResultPlaceholder(
title = title,
message = stringResource(R.string.no_results),
modifier = modifier,
)
} else {
ItemRow(
title = title,
items = r.items,
onClickItem = { index, item -> },
onLongClickItem = { _, _ -> },
modifier = modifier,
cardContent = { index: Int, item: SeerrSearchResult?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
SeasonCard(
title = item?.title ?: item?.name,
subtitle = null,
name = item?.title ?: item?.name,
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO
isFavorite = false,
isPlayed = false,
unplayedItemCount = -1,
playedPercentage = -1.0,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = Cards.height2x3,
modifier = mod,
)
},
)
}
}
}
}
}

View file

@ -2,9 +2,7 @@ package com.github.damontecres.wholphin.ui.seerr
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -14,10 +12,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.setValueOnMain
import dagger.hilt.android.lifecycle.HiltViewModel
@ -47,17 +47,32 @@ fun SeerrDiscoverPage(
) {
val movies by viewModel.discoverMovies.observeAsState(listOf())
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
modifier
.padding(16.dp),
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
items(movies) { movie ->
Text(
text = movie.title,
ItemRow(
title = "Movies",
items = movies,
onClickItem = { index, item -> },
onLongClickItem = { index, item -> },
cardContent = { index: Int, item: MovieResult?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
SeasonCard(
title = item?.title,
subtitle = null,
name = item?.title,
imageUrl = item?.posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }, // TODO
isFavorite = false,
isPlayed = false,
unplayedItemCount = -1,
playedPercentage = -1.0,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = Cards.height2x3,
modifier = mod,
)
},
)
}
}
}
}

View file

@ -715,7 +715,7 @@ components:
required:
- id
- mediaType
- title
# - title
properties:
id:
type: integer