Skip to content

Commit e08a026

Browse files
authored
Add search functionality to Jetcaster (#1298)
This pull request adds search functionality for Jetcaster. More specifically, the following changes are applied: - Add methods implementing podcast search feature to PodcastDao and PodcastStore - Add the search screen to the TV app
2 parents f53a56a + d5b2862 commit e08a026

File tree

10 files changed

+528
-4
lines changed

10 files changed

+528
-4
lines changed

Jetcaster/core/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,46 @@ abstract class PodcastsDao : BaseDao<Podcast> {
8989
limit: Int
9090
): Flow<List<PodcastWithExtraInfo>>
9191

92+
@Transaction
93+
@Query(
94+
"""
95+
SELECT podcasts.*, last_episode_date, (followed_entries.podcast_uri IS NOT NULL) AS is_followed
96+
FROM podcasts
97+
INNER JOIN (
98+
SELECT podcast_uri, MAX(published) AS last_episode_date FROM episodes GROUP BY podcast_uri
99+
) episodes ON podcasts.uri = episodes.podcast_uri
100+
INNER JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri
101+
WHERE podcasts.title LIKE '%' || :keyword || '%'
102+
ORDER BY datetime(last_episode_date) DESC
103+
LIMIT :limit
104+
"""
105+
)
106+
abstract fun searchPodcastByTitle(keyword: String, limit: Int): Flow<List<PodcastWithExtraInfo>>
107+
108+
@Transaction
109+
@Query(
110+
"""
111+
SELECT podcasts.*, last_episode_date, (followed_entries.podcast_uri IS NOT NULL) AS is_followed
112+
FROM podcasts
113+
INNER JOIN (
114+
SELECT episodes.podcast_uri, MAX(published) AS last_episode_date
115+
FROM episodes
116+
INNER JOIN podcast_category_entries ON episodes.podcast_uri = podcast_category_entries.podcast_uri
117+
WHERE category_id IN (:categoryIdList)
118+
GROUP BY episodes.podcast_uri
119+
) inner_query ON podcasts.uri = inner_query.podcast_uri
120+
LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = inner_query.podcast_uri
121+
WHERE podcasts.title LIKE '%' || :keyword || '%'
122+
ORDER BY datetime(last_episode_date) DESC
123+
LIMIT :limit
124+
"""
125+
)
126+
abstract fun searchPodcastByTitleAndCategory(
127+
keyword: String,
128+
categoryIdList: List<Long>,
129+
limit: Int
130+
): Flow<List<PodcastWithExtraInfo>>
131+
92132
@Query("SELECT COUNT(*) FROM podcasts")
93133
abstract suspend fun count(): Int
94134
}

Jetcaster/core/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.example.jetcaster.core.data.repository
1919
import com.example.jetcaster.core.data.database.dao.PodcastFollowedEntryDao
2020
import com.example.jetcaster.core.data.database.dao.PodcastsDao
2121
import com.example.jetcaster.core.data.database.dao.TransactionRunner
22+
import com.example.jetcaster.core.data.database.model.Category
2223
import com.example.jetcaster.core.data.database.model.Podcast
2324
import com.example.jetcaster.core.data.database.model.PodcastFollowedEntry
2425
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
@@ -46,6 +47,26 @@ interface PodcastStore {
4647
limit: Int = Int.MAX_VALUE
4748
): Flow<List<PodcastWithExtraInfo>>
4849

50+
/**
51+
* Returns a flow containing a list of podcasts such that its name partially matches
52+
* with the specified keyword
53+
*/
54+
fun searchPodcastByTitle(
55+
keyword: String,
56+
limit: Int = Int.MAX_VALUE
57+
): Flow<List<PodcastWithExtraInfo>>
58+
59+
/**
60+
* Return a flow containing a list of podcast such that it belongs to the any of categories
61+
* specified with categories parameter and its name partially matches with the specified
62+
* keyword.
63+
*/
64+
fun searchPodcastByTitleAndCategories(
65+
keyword: String,
66+
categories: List<Category>,
67+
limit: Int = Int.MAX_VALUE
68+
): Flow<List<PodcastWithExtraInfo>>
69+
4970
suspend fun togglePodcastFollowed(podcastUri: String)
5071

5172
suspend fun unfollowPodcast(podcastUri: String)
@@ -95,6 +116,22 @@ class LocalPodcastStore(
95116
return podcastDao.followedPodcastsSortedByLastEpisode(limit)
96117
}
97118

119+
override fun searchPodcastByTitle(
120+
keyword: String,
121+
limit: Int
122+
): Flow<List<PodcastWithExtraInfo>> {
123+
return podcastDao.searchPodcastByTitle(keyword, limit)
124+
}
125+
126+
override fun searchPodcastByTitleAndCategories(
127+
keyword: String,
128+
categories: List<Category>,
129+
limit: Int
130+
): Flow<List<PodcastWithExtraInfo>> {
131+
val categoryIdList = categories.map { it.id }
132+
return podcastDao.searchPodcastByTitleAndCategory(keyword, categoryIdList, limit)
133+
}
134+
98135
private suspend fun followPodcast(podcastUri: String) {
99136
podcastFollowedEntryDao.insert(PodcastFollowedEntry(podcastUri = podcastUri))
100137
}

Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestPodcastStore.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

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

19+
import com.example.jetcaster.core.data.database.model.Category
1920
import com.example.jetcaster.core.data.database.model.Podcast
2021
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
2122
import kotlinx.coroutines.flow.Flow
@@ -55,6 +56,37 @@ class TestPodcastStore : PodcastStore {
5556
}
5657
}
5758

59+
override fun searchPodcastByTitle(
60+
keyword: String,
61+
limit: Int
62+
): Flow<List<PodcastWithExtraInfo>> =
63+
podcastFlow.map { podcastList ->
64+
podcastList.filter {
65+
it.title.contains(keyword)
66+
}.map { p ->
67+
PodcastWithExtraInfo().apply {
68+
podcast = p
69+
isFollowed = true
70+
}
71+
}
72+
}
73+
74+
override fun searchPodcastByTitleAndCategories(
75+
keyword: String,
76+
categories: List<Category>,
77+
limit: Int
78+
): Flow<List<PodcastWithExtraInfo>> =
79+
podcastFlow.map { podcastList ->
80+
podcastList.filter {
81+
it.title.contains(keyword)
82+
}.map { p ->
83+
PodcastWithExtraInfo().apply {
84+
podcast = p
85+
isFollowed = true
86+
}
87+
}
88+
}
89+
5890
override suspend fun togglePodcastFollowed(podcastUri: String) {
5991
if (podcastUri in followedPodcasts) {
6092
followedPodcasts.remove(podcastUri)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.jetcaster.tv.model
18+
19+
import androidx.compose.runtime.Immutable
20+
import com.example.jetcaster.core.data.database.model.Category
21+
22+
data class CategorySelection(val category: Category, val isSelected: Boolean = false)
23+
24+
@Immutable
25+
data class CategorySelectionList(
26+
val member: List<CategorySelection>
27+
) : List<CategorySelection> by member

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ private fun Route(jetcasterAppState: JetcasterAppState) {
147147

148148
composable(Screen.Search.route) {
149149
SearchScreen(
150+
onPodcastSelected = {
151+
jetcasterAppState.showPodcastDetails(it.podcast.uri)
152+
},
150153
modifier = Modifier
151154
.padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues())
152155
.fillMaxSize()

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ private fun PodcastRow(
160160

161161
@OptIn(ExperimentalTvMaterial3Api::class)
162162
@Composable
163-
private fun PodcastCard(
163+
internal fun PodcastCard(
164164
podcast: Podcast,
165165
onClick: () -> Unit,
166166
modifier: Modifier = Modifier,

0 commit comments

Comments
 (0)