mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
PoC UI for search & discover
This commit is contained in:
parent
d08bd40a68
commit
0281d00907
6 changed files with 109 additions and 17 deletions
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.api.seerr
|
||||||
import com.github.damontecres.wholphin.api.seerr.infrastructure.ApiClient
|
import com.github.damontecres.wholphin.api.seerr.infrastructure.ApiClient
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class SeerrApiClient(
|
class SeerrApiClient(
|
||||||
val baseUrl: String,
|
val baseUrl: String,
|
||||||
|
|
@ -13,6 +14,7 @@ class SeerrApiClient(
|
||||||
okHttpClient
|
okHttpClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addInterceptor {
|
.addInterceptor {
|
||||||
|
Timber.d("SeerrApiClient: ${it.request().method} ${it.request().url}")
|
||||||
it.proceed(
|
it.proceed(
|
||||||
it
|
it
|
||||||
.request()
|
.request()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.services
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
|
@ -21,6 +22,8 @@ class SeerrApi(
|
||||||
)
|
)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
val active: Boolean get() = api.baseUrl.isNotNullOrBlank() && api.apiKey.isNotNullOrBlank()
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResul
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
typealias SearchResult = SearchGet200ResponseResultsInner
|
typealias SeerrSearchResult = SearchGet200ResponseResultsInner
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SeerrService
|
class SeerrService
|
||||||
|
|
@ -15,6 +15,8 @@ class SeerrService
|
||||||
) {
|
) {
|
||||||
private val api: SeerrApiClient get() = seerApi.api
|
private val api: SeerrApiClient get() = seerApi.api
|
||||||
|
|
||||||
|
val active: Boolean get() = seerApi.active
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
api.searchApi.discoverTvGet(1)
|
api.searchApi.discoverTvGet(1)
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +24,7 @@ class SeerrService
|
||||||
suspend fun search(
|
suspend fun search(
|
||||||
query: String,
|
query: String,
|
||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
): List<SearchResult> =
|
): List<SeerrSearchResult> =
|
||||||
api.searchApi
|
api.searchApi
|
||||||
.searchGet(query = query, page = page)
|
.searchGet(query = query, page = page)
|
||||||
.results
|
.results
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
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.Cards
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.cards.EpisodeCard
|
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.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
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.rememberPosition
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -73,11 +77,13 @@ class SearchViewModel
|
||||||
constructor(
|
constructor(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
private val seerrService: SeerrService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val movies = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
val movies = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
||||||
val series = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
val series = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
||||||
val episodes = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
val episodes = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
||||||
val collections = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
val collections = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
||||||
|
val seerrResults = MutableLiveData<SearchResult>(SearchResult.NoQuery)
|
||||||
|
|
||||||
private var currentQuery: String? = null
|
private var currentQuery: String? = null
|
||||||
|
|
||||||
|
|
@ -95,11 +101,13 @@ class SearchViewModel
|
||||||
searchInternal(query, BaseItemKind.SERIES, series)
|
searchInternal(query, BaseItemKind.SERIES, series)
|
||||||
searchInternal(query, BaseItemKind.EPISODE, episodes)
|
searchInternal(query, BaseItemKind.EPISODE, episodes)
|
||||||
searchInternal(query, BaseItemKind.BOX_SET, collections)
|
searchInternal(query, BaseItemKind.BOX_SET, collections)
|
||||||
|
searchSeerr(query)
|
||||||
} else {
|
} else {
|
||||||
movies.value = SearchResult.NoQuery
|
movies.value = SearchResult.NoQuery
|
||||||
series.value = SearchResult.NoQuery
|
series.value = SearchResult.NoQuery
|
||||||
episodes.value = SearchResult.NoQuery
|
episodes.value = SearchResult.NoQuery
|
||||||
collections.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) {
|
fun getHints(query: String) {
|
||||||
// TODO
|
// TODO
|
||||||
// api.searchApi.getSearchHints()
|
// api.searchApi.getSearchHints()
|
||||||
|
|
@ -151,12 +169,17 @@ sealed interface SearchResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
val items: List<BaseItem?>,
|
val items: List<BaseItem?>,
|
||||||
) : SearchResult
|
) : SearchResult
|
||||||
|
|
||||||
|
data class SuccessSeerr(
|
||||||
|
val items: List<SeerrSearchResult>,
|
||||||
|
) : SearchResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val MOVIE_ROW = 0
|
private const val MOVIE_ROW = 0
|
||||||
private const val COLLECTION_ROW = MOVIE_ROW + 1
|
private const val COLLECTION_ROW = MOVIE_ROW + 1
|
||||||
private const val SERIES_ROW = COLLECTION_ROW + 1
|
private const val SERIES_ROW = COLLECTION_ROW + 1
|
||||||
private const val EPISODE_ROW = SERIES_ROW + 1
|
private const val EPISODE_ROW = SERIES_ROW + 1
|
||||||
|
private const val SEERR_ROW = EPISODE_ROW + 1
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchPage(
|
fun SearchPage(
|
||||||
|
|
@ -171,6 +194,7 @@ fun SearchPage(
|
||||||
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
|
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
|
||||||
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
|
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
|
||||||
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
|
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
|
||||||
|
val seerrResults by viewModel.seerrResults.observeAsState(SearchResult.NoQuery)
|
||||||
|
|
||||||
val query = rememberTextFieldState()
|
val query = rememberTextFieldState()
|
||||||
val focusRequester = remember { FocusRequester() }
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@ package com.github.damontecres.wholphin.ui.seerr
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
|
@ -14,10 +12,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.Text
|
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -47,17 +47,32 @@ fun SeerrDiscoverPage(
|
||||||
) {
|
) {
|
||||||
val movies by viewModel.discoverMovies.observeAsState(listOf())
|
val movies by viewModel.discoverMovies.observeAsState(listOf())
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
LazyRow(
|
ItemRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
title = "Movies",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
items = movies,
|
||||||
) {
|
onClickItem = { index, item -> },
|
||||||
items(movies) { movie ->
|
onLongClickItem = { index, item -> },
|
||||||
Text(
|
cardContent = { index: Int, item: MovieResult?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
|
||||||
text = movie.title,
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -715,7 +715,7 @@ components:
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- mediaType
|
- mediaType
|
||||||
- title
|
# - title
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue