From 0281d0090725192757e08be21cfcd3ad26c227f6 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sun, 7 Dec 2025 20:25:24 -0500 Subject: [PATCH] PoC UI for search & discover --- .../wholphin/api/seerr/SeerrApiClient.kt | 2 + .../damontecres/wholphin/services/SeerrApi.kt | 3 + .../wholphin/services/SeerrService.kt | 6 +- .../wholphin/ui/main/SearchPage.kt | 70 +++++++++++++++++++ .../wholphin/ui/seerr/SeerrDiscoverPage.kt | 43 ++++++++---- app/src/main/seerr-api.yml | 2 +- 6 files changed, 109 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/api/seerr/SeerrApiClient.kt b/app/src/main/java/com/github/damontecres/wholphin/api/seerr/SeerrApiClient.kt index 64f42f2d..d5c3d74e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/api/seerr/SeerrApiClient.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/api/seerr/SeerrApiClient.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt index a009d53d..b3a3dcef 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt index 840587de..652f9ab3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt @@ -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 = + ): List = api.searchApi .searchGet(query = query, page = page) .results diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index 1a39c51d..386c7f4f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -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.NoQuery) val series = MutableLiveData(SearchResult.NoQuery) val episodes = MutableLiveData(SearchResult.NoQuery) val collections = MutableLiveData(SearchResult.NoQuery) + val seerrResults = MutableLiveData(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, ) : SearchResult + + data class SuccessSeerr( + val items: List, + ) : 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, + ) + }, + ) + } + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/seerr/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/seerr/SeerrDiscoverPage.kt index c0a5e839..6c65a656 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/seerr/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/seerr/SeerrDiscoverPage.kt @@ -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, ) - } - } + }, + ) } } diff --git a/app/src/main/seerr-api.yml b/app/src/main/seerr-api.yml index 8555e072..57a495d7 100644 --- a/app/src/main/seerr-api.yml +++ b/app/src/main/seerr-api.yml @@ -715,7 +715,7 @@ components: required: - id - mediaType - - title + # - title properties: id: type: integer