Skip to content

Add search functionality to Jetcaster #1298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,46 @@ abstract class PodcastsDao : BaseDao<Podcast> {
limit: Int
): Flow<List<PodcastWithExtraInfo>>

@Transaction
@Query(
"""
SELECT podcasts.*, last_episode_date, (followed_entries.podcast_uri IS NOT NULL) AS is_followed
FROM podcasts
INNER JOIN (
SELECT podcast_uri, MAX(published) AS last_episode_date FROM episodes GROUP BY podcast_uri
) episodes ON podcasts.uri = episodes.podcast_uri
INNER JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri
WHERE podcasts.title LIKE '%' || :keyword || '%'
ORDER BY datetime(last_episode_date) DESC
LIMIT :limit
"""
)
abstract fun searchPodcastByTitle(keyword: String, limit: Int): Flow<List<PodcastWithExtraInfo>>

@Transaction
@Query(
"""
SELECT podcasts.*, last_episode_date, (followed_entries.podcast_uri IS NOT NULL) AS is_followed
FROM podcasts
INNER JOIN (
SELECT episodes.podcast_uri, MAX(published) AS last_episode_date
FROM episodes
INNER JOIN podcast_category_entries ON episodes.podcast_uri = podcast_category_entries.podcast_uri
WHERE category_id IN (:categoryIdList)
GROUP BY episodes.podcast_uri
) inner_query ON podcasts.uri = inner_query.podcast_uri
LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = inner_query.podcast_uri
WHERE podcasts.title LIKE '%' || :keyword || '%'
ORDER BY datetime(last_episode_date) DESC
LIMIT :limit
"""
)
abstract fun searchPodcastByTitleAndCategory(
keyword: String,
categoryIdList: List<Long>,
limit: Int
): Flow<List<PodcastWithExtraInfo>>

@Query("SELECT COUNT(*) FROM podcasts")
abstract suspend fun count(): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.example.jetcaster.core.data.repository
import com.example.jetcaster.core.data.database.dao.PodcastFollowedEntryDao
import com.example.jetcaster.core.data.database.dao.PodcastsDao
import com.example.jetcaster.core.data.database.dao.TransactionRunner
import com.example.jetcaster.core.data.database.model.Category
import com.example.jetcaster.core.data.database.model.Podcast
import com.example.jetcaster.core.data.database.model.PodcastFollowedEntry
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
Expand Down Expand Up @@ -46,6 +47,26 @@ interface PodcastStore {
limit: Int = Int.MAX_VALUE
): Flow<List<PodcastWithExtraInfo>>

/**
* Returns a flow containing a list of podcasts such that its name partially matches
* with the specified keyword
*/
fun searchPodcastByTitle(
keyword: String,
limit: Int = Int.MAX_VALUE
): Flow<List<PodcastWithExtraInfo>>

/**
* Return a flow containing a list of podcast such that it belongs to the any of categories
* specified with categories parameter and its name partially matches with the specified
* keyword.
*/
fun searchPodcastByTitleAndCategories(
keyword: String,
categories: List<Category>,
limit: Int = Int.MAX_VALUE
): Flow<List<PodcastWithExtraInfo>>

suspend fun togglePodcastFollowed(podcastUri: String)

suspend fun unfollowPodcast(podcastUri: String)
Expand Down Expand Up @@ -95,6 +116,22 @@ class LocalPodcastStore(
return podcastDao.followedPodcastsSortedByLastEpisode(limit)
}

override fun searchPodcastByTitle(
keyword: String,
limit: Int
): Flow<List<PodcastWithExtraInfo>> {
return podcastDao.searchPodcastByTitle(keyword, limit)
}

override fun searchPodcastByTitleAndCategories(
keyword: String,
categories: List<Category>,
limit: Int
): Flow<List<PodcastWithExtraInfo>> {
val categoryIdList = categories.map { it.id }
return podcastDao.searchPodcastByTitleAndCategory(keyword, categoryIdList, limit)
}

private suspend fun followPodcast(podcastUri: String) {
podcastFollowedEntryDao.insert(PodcastFollowedEntry(podcastUri = podcastUri))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.example.jetcaster.core.data.repository

import com.example.jetcaster.core.data.database.model.Category
import com.example.jetcaster.core.data.database.model.Podcast
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -55,6 +56,37 @@ class TestPodcastStore : PodcastStore {
}
}

override fun searchPodcastByTitle(
keyword: String,
limit: Int
): Flow<List<PodcastWithExtraInfo>> =
podcastFlow.map { podcastList ->
podcastList.filter {
it.title.contains(keyword)
}.map { p ->
PodcastWithExtraInfo().apply {
podcast = p
isFollowed = true
}
}
}

override fun searchPodcastByTitleAndCategories(
keyword: String,
categories: List<Category>,
limit: Int
): Flow<List<PodcastWithExtraInfo>> =
podcastFlow.map { podcastList ->
podcastList.filter {
it.title.contains(keyword)
}.map { p ->
PodcastWithExtraInfo().apply {
podcast = p
isFollowed = true
}
}
}

override suspend fun togglePodcastFollowed(podcastUri: String) {
if (podcastUri in followedPodcasts) {
followedPodcasts.remove(podcastUri)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.jetcaster.tv.model

import androidx.compose.runtime.Immutable
import com.example.jetcaster.core.data.database.model.Category

data class CategorySelection(val category: Category, val isSelected: Boolean = false)

@Immutable
data class CategorySelectionList(
val member: List<CategorySelection>
) : List<CategorySelection> by member
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ private fun Route(jetcasterAppState: JetcasterAppState) {

composable(Screen.Search.route) {
SearchScreen(
onPodcastSelected = {
jetcasterAppState.showPodcastDetails(it.podcast.uri)
},
modifier = Modifier
.padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues())
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ private fun PodcastRow(

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun PodcastCard(
internal fun PodcastCard(
podcast: Podcast,
onClick: () -> Unit,
modifier: Modifier = Modifier,
Expand Down
Loading