6.12.4 commit

This commit is contained in:
Xilin Jia 2024-10-25 14:45:39 +01:00
parent 7b6d976c9a
commit 15bc1efdca
22 changed files with 303 additions and 676 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020281
versionName "6.12.3"
versionCode 3020282
versionName "6.12.4"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -89,6 +89,7 @@ class Episode : RealmObject {
var isFavorite: Boolean = (rating == Rating.FAVORITE.code)
private set
@FullText
var comment: String = ""
@Ignore

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
import ac.mdiq.podcini.storage.model.MediaType.Companion.AUDIO_APPLICATION_MIME_STRINGS
import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable {
@ -20,29 +19,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
// fun matches(item: Episode): Boolean {
// when {
// showNew && !item.isNew -> return false
// showPlayed && item.playState < PlayState.PLAYED.code -> return false
// showUnplayed && item.playState >= PlayState.PLAYED.code -> return false
// properties.contains(States.paused.name) && !item.isInProgress -> return false
// properties.contains(States.not_paused.name) && item.isInProgress -> return false
// showDownloaded && !item.isDownloaded -> return false
// showNotDownloaded && item.isDownloaded -> return false
// properties.contains(States.auto_downloadable.name) && !item.isAutoDownloadEnabled -> return false
// properties.contains(States.not_auto_downloadable.name) && item.isAutoDownloadEnabled -> return false
// properties.contains(States.has_media.name) && item.media == null -> return false
// properties.contains(States.no_media.name) && item.media != null -> return false
// properties.contains(States.has_comments.name) && item.comment.isEmpty() -> return false
// properties.contains(States.no_comments.name) && item.comment.isNotEmpty() -> return false
// showIsFavorite && !item.isFavorite -> return false
// showNotFavorite && item.isFavorite -> return false
// showQueued && !inAnyQueue(item) -> return false
// showNotQueued && inAnyQueue(item) -> return false
// else -> return true
// }
// }
// filter on queues does not have a query string so it's not applied on query results, need to filter separately
fun matchesForQueues(item: Episode): Boolean {
return when {
@ -64,10 +40,10 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.unknown.name)) mediaTypeQuerys.add(" media == nil OR media.mimeType == nil OR media.mimeType == '' ")
if (properties.contains(States.audio.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'audio' ")
if (properties.contains(States.video.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'video' ")
if (properties.contains(States.audio_app.name)) mediaTypeQuerys.add(" media.mimeType IN ${AUDIO_APPLICATION_MIME_STRINGS.toList()} ")
if (properties.contains(States.audio_app.name)) mediaTypeQuerys.add(" media.mimeType IN {\"application/ogg\", \"application/opus\", \"application/x-flac\"} ")
if (mediaTypeQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + mediaTypeQuerys[0])
if (mediaTypeQuerys.size > 1) for (r in statements.subList(1, mediaTypeQuerys.size)) {
if (mediaTypeQuerys.size > 1) for (r in mediaTypeQuerys.subList(1, mediaTypeQuerys.size)) {
query.append(" OR ")
query.append(r)
}
@ -84,7 +60,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.favorite.name)) ratingQuerys.add(" rating == ${Rating.FAVORITE.code} ")
if (ratingQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + ratingQuerys[0])
if (ratingQuerys.size > 1) for (r in statements.subList(1, ratingQuerys.size)) {
if (ratingQuerys.size > 1) for (r in ratingQuerys.subList(1, ratingQuerys.size)) {
query.append(" OR ")
query.append(r)
}
@ -106,7 +82,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.ignored.name)) stateQuerys.add(" playState == ${PlayState.IGNORED.code} ")
if (stateQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + stateQuerys[0])
if (stateQuerys.size > 1) for (r in statements.subList(1, stateQuerys.size)) {
if (stateQuerys.size > 1) for (r in stateQuerys.subList(1, stateQuerys.size)) {
query.append(" OR ")
query.append(r)
}
@ -198,11 +174,10 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
favorite,
}
enum class EpisodesFilterGroup(val nameRes: Int, vararg values: ItemProperties) {
enum class EpisodesFilterGroup(val nameRes: Int, vararg values_: ItemProperties) {
// PLAYED(ItemProperties(R.string.hide_played_episodes_label, States.played.name), ItemProperties(R.string.not_played, States.unplayed.name)),
// PAUSED(ItemProperties(R.string.hide_paused_episodes_label, States.paused.name), ItemProperties(R.string.not_paused, States.not_paused.name)),
// FAVORITE(ItemProperties(R.string.hide_is_favorite_label, States.is_favorite.name), ItemProperties(R.string.not_favorite, States.not_favorite.name)),
MEDIA(R.string.has_media, ItemProperties(R.string.yes, States.has_media.name), ItemProperties(R.string.no, States.no_media.name)),
RATING(R.string.rating_label, ItemProperties(R.string.unrated, States.unrated.name),
ItemProperties(R.string.trash, States.trash.name),
ItemProperties(R.string.bad, States.bad.name),
@ -224,6 +199,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
),
OPINION(R.string.has_comments, ItemProperties(R.string.yes, States.has_comments.name), ItemProperties(R.string.no, States.no_comments.name)),
// QUEUED(ItemProperties(R.string.queued_label, States.queued.name), ItemProperties(R.string.not_queued_label, States.not_queued.name)),
MEDIA(R.string.has_media, ItemProperties(R.string.yes, States.has_media.name), ItemProperties(R.string.no, States.no_media.name)),
DOWNLOADED(R.string.downloaded_label, ItemProperties(R.string.yes, States.downloaded.name), ItemProperties(R.string.no, States.not_downloaded.name)),
CHAPTERS(R.string.has_chapters, ItemProperties(R.string.yes, States.has_chapters.name), ItemProperties(R.string.no, States.no_chapters.name)),
MEDIA_TYPE(R.string.media_type, ItemProperties(R.string.unknown, States.unknown.name),
@ -233,10 +209,9 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
),
AUTO_DOWNLOADABLE(R.string.auto_downloadable_label, ItemProperties(R.string.yes, States.auto_downloadable.name), ItemProperties(R.string.no, States.not_auto_downloadable.name));
@JvmField
val values: Array<ItemProperties> = arrayOf(*values)
val values: Array<ItemProperties> = arrayOf(*values_)
class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String)
class ItemProperties(val displayName: Int, val filterId: String)
}
companion object {

View File

@ -49,6 +49,7 @@ class Feed : RealmObject {
@FullText
var author: String? = null
var imageUrl: String? = null
var episodes: RealmList<Episode> = realmListOf()
@ -98,6 +99,7 @@ class Feed : RealmObject {
var rating: Int = Rating.NEUTRAL.code
@FullText
var comment: String = ""
/**

View File

@ -11,37 +11,17 @@ class FeedFilter(vararg properties_: String) : Serializable {
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
// fun matches(feed: Feed): Boolean {
// when {
// properties.contains(States.keepUpdated.name) && feed.preferences?.keepUpdated != true -> return false
// properties.contains(States.not_keepUpdated.name) && feed.preferences?.keepUpdated != false -> return false
// properties.contains(States.global_playSpeed.name) && feed.preferences?.playSpeed != SPEED_USE_GLOBAL -> return false
// properties.contains(States.custom_playSpeed.name) && feed.preferences?.playSpeed == SPEED_USE_GLOBAL -> return false
// properties.contains(States.has_comments.name) && feed.comment.isEmpty() -> return false
// properties.contains(States.no_comments.name) && feed.comment.isNotEmpty() -> return false
// properties.contains(States.has_skips.name) && feed.preferences?.introSkip == 0 && feed.preferences?.endingSkip == 0 -> return false
// properties.contains(States.no_skips.name) && (feed.preferences?.introSkip != 0 || feed.preferences?.endingSkip != 0) -> return false
// properties.contains(States.global_auto_delete.name) && feed.preferences?.autoDeleteAction != FeedPreferences.AutoDeleteAction.GLOBAL -> return false
// properties.contains(States.always_auto_delete.name) && feed.preferences?.autoDeleteAction != FeedPreferences.AutoDeleteAction.ALWAYS -> return false
// properties.contains(States.never_auto_delete.name) && feed.preferences?.autoDeleteAction != FeedPreferences.AutoDeleteAction.NEVER -> return false
// properties.contains(States.autoDownload.name) && feed.preferences?.autoDownload != true -> return false
// properties.contains(States.not_autoDownload.name) && feed.preferences?.autoDownload != false -> return false
// properties.contains(States.unrated.name) && feed.rating != Rating.UNRATED.code -> return false
// properties.contains(States.trash.name) && feed.rating != Rating.TRASH.code -> return false
// properties.contains(States.bad.name) && feed.rating != Rating.BAD.code -> return false
// properties.contains(States.neutral.name) && feed.rating != Rating.NEUTRAL.code -> return false
// properties.contains(States.good.name) && feed.rating != Rating.GOOD.code -> return false
// properties.contains(States.favorite.name) && feed.rating != Rating.FAVORITE.code -> return false
// else -> return true
// }
// }
fun queryString(): String {
val statements: MutableList<String> = mutableListOf()
when {
properties.contains(States.keepUpdated.name) -> statements.add("preferences.keepUpdated == true ")
properties.contains(States.not_keepUpdated.name) -> statements.add(" preferences.keepUpdated == false ")
}
when {
properties.contains(States.pref_streaming.name) -> statements.add("preferences.prefStreamOverDownload == true ")
properties.contains(States.not_pref_streaming.name) -> statements.add(" preferences.prefStreamOverDownload == false ")
}
when {
properties.contains(States.global_playSpeed.name) -> statements.add(" preferences.playSpeed == $SPEED_USE_GLOBAL ")
properties.contains(States.custom_playSpeed.name) -> statements.add(" preferences.playSpeed != $SPEED_USE_GLOBAL ")
@ -64,7 +44,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
}
when {
properties.contains(States.youtube.name) -> statements.add(" downloadUrl CONTAINS[c] 'youtube' OR link CONTAINS[c] 'youtube' OR downloadUrl CONTAINS[c] 'youtu.be' OR link CONTAINS[c] 'youtu.be' ")
properties.contains(States.rss.name) -> statements.add(" downloadUrl NOT CONTAINS[c] 'youtube' AND link NOT CONTAINS[c] 'youtube' AND downloadUrl NOT CONTAINS[c] 'youtu.be' AND link NOT CONTAINS[c] 'youtu.be' ")
properties.contains(States.rss.name) -> statements.add(" !(downloadUrl CONTAINS[c] 'youtube' OR link CONTAINS[c] 'youtube' OR downloadUrl CONTAINS[c] 'youtu.be' OR link CONTAINS[c] 'youtu.be') ")
}
val ratingQuerys = mutableListOf<String>()
@ -76,7 +56,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.favorite.name)) ratingQuerys.add(" rating == ${Rating.FAVORITE.code} ")
if (ratingQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + ratingQuerys[0])
if (ratingQuerys.size > 1) for (r in statements.subList(1, ratingQuerys.size)) {
if (ratingQuerys.size > 1) for (r in ratingQuerys.subList(1, ratingQuerys.size)) {
query.append(" OR ")
query.append(r)
}
@ -90,7 +70,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.never_auto_delete.name)) audoDeleteQuerys.add(" preferences.playSpeed == ${FeedPreferences.AutoDeleteAction.NEVER.code} ")
if (audoDeleteQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + audoDeleteQuerys[0])
if (audoDeleteQuerys.size > 1) for (r in statements.subList(1, audoDeleteQuerys.size)) {
if (audoDeleteQuerys.size > 1) for (r in audoDeleteQuerys.subList(1, audoDeleteQuerys.size)) {
query.append(" OR ")
query.append(r)
}
@ -110,6 +90,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
query.append(r)
}
query.append(") ")
Logd("queryString", "${query}")
return query.toString()
}
@ -117,6 +98,8 @@ class FeedFilter(vararg properties_: String) : Serializable {
enum class States {
keepUpdated,
not_keepUpdated,
pref_streaming,
not_pref_streaming,
global_playSpeed,
custom_playSpeed,
has_skips,
@ -142,14 +125,9 @@ class FeedFilter(vararg properties_: String) : Serializable {
favorite,
}
enum class FeedFilterGroup(val nameRes: Int, vararg values: ItemProperties) {
enum class FeedFilterGroup(val nameRes: Int, vararg values_: ItemProperties) {
KEEP_UPDATED(R.string.keep_updated, ItemProperties(R.string.yes, States.keepUpdated.name), ItemProperties(R.string.no, States.not_keepUpdated.name)),
PLAY_SPEED(R.string.play_speed, ItemProperties(R.string.global_speed, States.global_playSpeed.name), ItemProperties(R.string.custom_speed, States.custom_playSpeed.name)),
OPINION(R.string.commented, ItemProperties(R.string.yes, States.has_comments.name), ItemProperties(R.string.no, States.no_comments.name)),
HAS_VIDEO(R.string.has_video, ItemProperties(R.string.yes, States.has_video.name), ItemProperties(R.string.no, States.no_video.name)),
ORIGIN(R.string.feed_origin, ItemProperties(R.string.youtube, States.youtube.name), ItemProperties(R.string.rss, States.rss.name)),
TYPE(R.string.feed_type, ItemProperties(R.string.synthetic, States.synthetic.name), ItemProperties(R.string.normal, States.normal.name)),
SKIPS(R.string.has_skips, ItemProperties(R.string.yes, States.has_skips.name), ItemProperties(R.string.no, States.no_skips.name)),
RATING(R.string.rating_label, ItemProperties(R.string.unrated, States.unrated.name),
ItemProperties(R.string.trash, States.trash.name),
ItemProperties(R.string.bad, States.bad.name),
@ -157,15 +135,20 @@ class FeedFilter(vararg properties_: String) : Serializable {
ItemProperties(R.string.good, States.good.name),
ItemProperties(R.string.favorite, States.favorite.name),
),
HAS_VIDEO(R.string.has_video, ItemProperties(R.string.yes, States.has_video.name), ItemProperties(R.string.no, States.no_video.name)),
PLAY_SPEED(R.string.play_speed, ItemProperties(R.string.global_speed, States.global_playSpeed.name), ItemProperties(R.string.custom_speed, States.custom_playSpeed.name)),
ORIGIN(R.string.feed_origin, ItemProperties(R.string.youtube, States.youtube.name), ItemProperties(R.string.rss, States.rss.name)),
TYPE(R.string.feed_type, ItemProperties(R.string.synthetic, States.synthetic.name), ItemProperties(R.string.normal, States.normal.name)),
SKIPS(R.string.has_skips, ItemProperties(R.string.yes, States.has_skips.name), ItemProperties(R.string.no, States.no_skips.name)),
AUTO_DELETE(R.string.auto_delete, ItemProperties(R.string.always, States.always_auto_delete.name),
ItemProperties(R.string.never, States.never_auto_delete.name),
ItemProperties(R.string.global, States.global_auto_delete.name), ),
PREF_STREAMING(R.string.pref_stream_over_download_title, ItemProperties(R.string.yes, States.pref_streaming.name), ItemProperties(R.string.no, States.not_pref_streaming.name)),
AUTO_DOWNLOAD(R.string.auto_download, ItemProperties(R.string.yes, States.autoDownload.name), ItemProperties(R.string.no, States.not_autoDownload.name));
@JvmField
val values: Array<ItemProperties> = arrayOf(*values)
val values: Array<ItemProperties> = arrayOf(*values_)
class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String)
class ItemProperties(val displayName: Int, val filterId: String)
}
companion object {

View File

@ -4,12 +4,6 @@ enum class MediaType {
AUDIO, VIDEO, UNKNOWN;
companion object {
// private val AUDIO_APPLICATION_MIME_STRINGS: Set<String> = HashSet(mutableListOf(
// "application/ogg",
// "application/opus",
// "application/x-flac"
// ))
val AUDIO_APPLICATION_MIME_STRINGS: HashSet<String> = hashSetOf(
"application/ogg",
"application/opus",

View File

@ -40,9 +40,6 @@ import org.apache.commons.io.input.BOMInputStream
import java.io.InputStreamReader
import java.io.Reader
/**
* Activity for Opml Import.
*/
class OpmlImportActivity : AppCompatActivity() {
private var uri: Uri? = null
private var _binding: OpmlSelectionBinding? = null
@ -82,6 +79,7 @@ class OpmlImportActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
_binding = OpmlSelectionBinding.inflate(layoutInflater)
setContentView(binding.root)
Logd(TAG, "onCreate")
binding.feedlist.choiceMode = ListView.CHOICE_MODE_MULTIPLE
binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
@ -106,7 +104,6 @@ class OpmlImportActivity : AppCompatActivity() {
}
binding.butConfirm.setOnClickListener {
binding.progressBar.visibility = View.VISIBLE
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {

View File

@ -943,7 +943,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().height(500.dp), shape = RoundedCornerShape(16.dp)) {
Surface(modifier = Modifier.fillMaxWidth().height(350.dp), color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {

View File

@ -1,147 +0,0 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.NonlazyGrid
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
// TODO: to be removed
abstract class EpisodeFilterDialog : BottomSheetDialogFragment() {
var filter: EpisodeFilter? = null
val filtersDisabled: MutableSet<EpisodeFilter.EpisodesFilterGroup> = mutableSetOf()
private val filterValues: MutableSet<String> = mutableSetOf()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val composeView = ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
MainView()
}
}
}
return composeView
}
@Composable
fun MainView() {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
var selectNone by remember { mutableStateOf(false) }
for (item in EpisodeFilter.EpisodesFilterGroup.entries) {
if (item in filtersDisabled) continue
if (item.values.size == 2) {
Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
var selectedIndex by remember { mutableStateOf(-1) }
if (selectNone) selectedIndex = -1
LaunchedEffect(Unit) {
if (filter != null) {
if (item.values[0].filterId in filter!!.properties) selectedIndex = 0
else if (item.values[1].filterId in filter!!.properties) selectedIndex = 1
}
}
Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.padding(end = 10.dp))
Spacer(Modifier.weight(0.3f))
OutlinedButton(
modifier = Modifier.padding(2.dp), border = BorderStroke(2.dp, if (selectedIndex != 0) textColor else Color.Green),
onClick = {
if (selectedIndex != 0) {
selectNone = false
selectedIndex = 0
filterValues.add(item.values[0].filterId)
filterValues.remove(item.values[1].filterId)
} else {
selectedIndex = -1
filterValues.remove(item.values[0].filterId)
}
onFilterChanged(filterValues)
},
) {
Text(text = stringResource(item.values[0].displayName), color = textColor)
}
Spacer(Modifier.weight(0.1f))
OutlinedButton(
modifier = Modifier.padding(2.dp), border = BorderStroke(2.dp, if (selectedIndex != 1) textColor else Color.Green),
onClick = {
if (selectedIndex != 1) {
selectNone = false
selectedIndex = 1
filterValues.add(item.values[1].filterId)
filterValues.remove(item.values[0].filterId)
} else {
selectedIndex = -1
filterValues.remove(item.values[1].filterId)
}
onFilterChanged(filterValues)
},
) {
Text(text = stringResource(item.values[1].displayName), color = textColor)
}
Spacer(Modifier.weight(0.5f))
}
} else {
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor)
NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
var selected by remember { mutableStateOf(false) }
if (selectNone) selected = false
OutlinedButton(modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(),
border = BorderStroke(2.dp, if (selected) Color.Green else textColor),
onClick = {
selectNone = false
selected = !selected
if (selected) filterValues.add(item.values[index].filterId)
else filterValues.remove(item.values[index].filterId)
onFilterChanged(filterValues)
},
) {
Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor)
}
}
}
}
}
Row {
Spacer(Modifier.weight(0.3f))
Button(onClick = {
selectNone = true
onFilterChanged(setOf(""))
}) {
Text(stringResource(R.string.reset))
}
Spacer(Modifier.weight(0.4f))
Button(onClick = {
dismiss()
}) {
Text(stringResource(R.string.close_label))
}
Spacer(Modifier.weight(0.3f))
}
}
}
abstract fun onFilterChanged(newFilterValues: Set<String>)
}

View File

@ -93,12 +93,12 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
showFilterDialog = true
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
}
R.id.action_favorites -> {
val filter = getFilter().properties.toMutableSet()
if (filter.contains(EpisodeFilter.States.is_favorite.name)) filter.remove(EpisodeFilter.States.is_favorite.name)
else filter.add(EpisodeFilter.States.is_favorite.name)
onFilterChanged(FlowEvent.AllEpisodesFilterEvent(HashSet(filter)))
}
// R.id.action_favorites -> {
// val filter = getFilter().properties.toMutableSet()
// if (filter.contains(EpisodeFilter.States.is_favorite.name)) filter.remove(EpisodeFilter.States.is_favorite.name)
// else filter.add(EpisodeFilter.States.is_favorite.name)
// onFilterChanged(FlowEvent.AllEpisodesFilterEvent(HashSet(filter)))
// }
R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
else -> return false
@ -117,7 +117,7 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.AllEpisodesFilterEvent -> onFilterChanged(event)
// is FlowEvent.AllEpisodesFilterEvent -> onFilterChanged(event)
is FlowEvent.AllEpisodesSortEvent -> {
page = 1
loadItems()
@ -128,11 +128,11 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
}
}
private fun onFilterChanged(event: FlowEvent.AllEpisodesFilterEvent) {
prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",")
page = 1
loadItems()
}
// private fun onFilterChanged(event: FlowEvent.AllEpisodesFilterEvent) {
// prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",")
// page = 1
// loadItems()
// }
override fun updateToolbar() {
swipeActions.setFilter(getFilter())
@ -142,11 +142,14 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
emptyView.setMessage(R.string.no_all_episodes_filtered_label)
} else emptyView.setMessage(R.string.no_all_episodes_label)
infoBarText.value = info
toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border)
// toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border)
}
override fun onFilterChanged(filterValues: Set<String>) {
EventFlow.postEvent(FlowEvent.AllEpisodesFilterEvent(filterValues))
// EventFlow.postEvent(FlowEvent.AllEpisodesFilterEvent(filterValues))
prefFilterAllEpisodes = StringUtils.join(filterValues, ",")
page = 1
loadItems()
}
class AllEpisodesSortDialog : EpisodeSortDialog() {

View File

@ -100,7 +100,12 @@ import java.util.*
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(),
filtersDisabled = mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA),
onDismissRequest = { showFilterDialog = false } ) {
EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(it))
// EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(it))
val fSet = it.toMutableSet()
fSet.add(EpisodeFilter.States.downloaded.name)
prefFilterDownloads = StringUtils.join(fSet, ",")
Logd(TAG, "onFilterChanged: $prefFilterDownloads")
loadItems()
}
Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
@ -261,7 +266,7 @@ import java.util.*
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event)
// is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event)
is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event)
is FlowEvent.PlayerSettingsEvent -> loadItems()
is FlowEvent.DownloadLogEvent -> loadItems()
@ -283,13 +288,13 @@ import java.util.*
// }
}
private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) {
val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf()
fSet.add(EpisodeFilter.States.downloaded.name)
prefFilterDownloads = StringUtils.join(fSet, ",")
Logd(TAG, "onFilterChanged: $prefFilterDownloads")
loadItems()
}
// private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) {
// val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf()
// fSet.add(EpisodeFilter.States.downloaded.name)
// prefFilterDownloads = StringUtils.join(fSet, ",")
// Logd(TAG, "onFilterChanged: $prefFilterDownloads")
// loadItems()
// }
private fun addEmptyView() {
emptyView = EmptyViewHandler(requireContext())
@ -434,21 +439,6 @@ import java.util.*
}
}
// class DownloadsFilterDialog : EpisodeFilterDialog() {
// override fun onFilterChanged(newFilterValues: Set<String>) {
// EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(newFilterValues))
// }
// companion object {
// fun newInstance(filter: EpisodeFilter?): DownloadsFilterDialog {
// val dialog = DownloadsFilterDialog()
// dialog.filter = filter
// dialog.filtersDisabled.add(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED)
// dialog.filtersDisabled.add(EpisodeFilter.EpisodesFilterGroup.MEDIA)
// return dialog
// }
// }
// }
companion object {
val TAG = DownloadsFragment::class.simpleName ?: "Anonymous"

View File

@ -42,7 +42,6 @@ import kotlin.math.min
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
toolbar.inflateMenu(R.menu.playback_history)
toolbar.setTitle(R.string.playback_history_label)
@ -50,7 +49,6 @@ import kotlin.math.min
emptyView.setIcon(R.drawable.ic_history)
emptyView.setTitle(R.string.no_history_head_label)
emptyView.setMessage(R.string.no_history_label)
return root
}
@ -130,7 +128,7 @@ import kotlin.math.min
emptyView.setMessage(R.string.no_all_episodes_filtered_label)
} else emptyView.setMessage(R.string.no_all_episodes_label)
infoBarText.value = info
toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border)
// toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border)
}
private var eventSink: Job? = null

View File

@ -720,14 +720,14 @@ class OnlineFeedFragment : Fragment() {
updateToolbar()
return root
}
override fun onStart() {
super.onStart()
procFlowEvents()
}
override fun onStop() {
super.onStop()
cancelFlowEvents()
}
// override fun onStart() {
// super.onStart()
//// procFlowEvents()
// }
// override fun onStop() {
// super.onStop()
//// cancelFlowEvents()
// }
override fun onDestroyView() {
episodeList.clear()
super.onDestroyView()
@ -750,7 +750,7 @@ class OnlineFeedFragment : Fragment() {
binding.toolbar.menu.findItem(R.id.episodes_sort).setVisible(false)
// binding.toolbar.menu.findItem(R.id.refresh_item).setVisible(false)
binding.toolbar.menu.findItem(R.id.action_search).setVisible(false)
binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false)
// binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false)
binding.toolbar.menu.findItem(R.id.filter_items).setVisible(false)
infoBarText.value = "${episodes.size} episodes"
}
@ -760,23 +760,23 @@ class OnlineFeedFragment : Fragment() {
else -> return false
}
}
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink != null) return
eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.AllEpisodesFilterEvent -> page = 1
else -> {}
}
}
}
}
// private var eventSink: Job? = null
// private fun cancelFlowEvents() {
// eventSink?.cancel()
// eventSink = null
// }
// private fun procFlowEvents() {
// if (eventSink != null) return
// eventSink = lifecycleScope.launch {
// EventFlow.events.collectLatest { event ->
// Logd(TAG, "Received event: ${event.TAG}")
// when (event) {
// is FlowEvent.AllEpisodesFilterEvent -> page = 1
// else -> {}
// }
// }
// }
// }
companion object {
const val PREF_NAME: String = "EpisodesListFragment"

View File

@ -2,49 +2,60 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.HorizontalFeedItemBinding
import ac.mdiq.podcini.databinding.SearchFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
import ac.mdiq.podcini.ui.compose.EpisodeVM
import ac.mdiq.podcini.ui.dialog.CustomFeedNameDialog
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.view.SquareImageView
import ac.mdiq.podcini.ui.compose.NonlazyGrid
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.Pair
import android.view.*
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.cardview.widget.CardView
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import coil.load
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip
@ -53,7 +64,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import java.text.NumberFormat
/**
* Performs a search operation on all feeds or one specific feed and displays the search result.
@ -63,12 +74,11 @@ class SearchFragment : Fragment() {
private var _binding: SearchFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var adapterFeeds: HorizontalFeedListAdapter
private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var searchView: SearchView
private lateinit var chip: Chip
private lateinit var automaticSearchDebouncer: Handler
private val resultFeeds = mutableStateListOf<Feed>()
private val results = mutableListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>()
@ -86,34 +96,20 @@ class SearchFragment : Fragment() {
Logd(TAG, "fragment onCreateView")
setupToolbar(binding.toolbar)
binding.lazyColumn.setContent {
binding.resultsListView.setContent {
CustomTheme(requireContext()) {
EpisodeLazyColumn(activity as MainActivity, vms = vms)
Column {
CriteriaList()
FeedsRow()
EpisodeLazyColumn(activity as MainActivity, vms = vms)
}
}
}
val recyclerViewFeeds = binding.recyclerViewFeeds
val layoutManagerFeeds = LinearLayoutManager(activity)
layoutManagerFeeds.orientation = RecyclerView.HORIZONTAL
recyclerViewFeeds.layoutManager = layoutManagerFeeds
adapterFeeds = object : HorizontalFeedListAdapter(activity as MainActivity) {
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(contextMenu, view, contextMenuInfo)
MenuItemUtils.setOnClickListeners(contextMenu) { item: MenuItem -> this@SearchFragment.onContextItemSelected(item) }
}
}
recyclerViewFeeds.adapter = adapterFeeds
emptyViewHandler = EmptyViewHandler(requireContext())
// emptyViewHandler.attachToRecyclerView(recyclerView)
emptyViewHandler.setIcon(R.drawable.ic_search)
emptyViewHandler.setTitle(R.string.search_status_no_results)
emptyViewHandler.setMessage(R.string.type_to_search)
chip = binding.feedTitleChip
chip.setOnCloseIconClickListener {
requireArguments().putLong(ARG_FEED, 0)
searchWithProgressBar()
search()
}
chip.visibility = if (requireArguments().getLong(ARG_FEED, 0) == 0L) View.GONE else View.VISIBLE
chip.text = requireArguments().getString(ARG_FEED_NAME, "")
@ -139,6 +135,7 @@ class SearchFragment : Fragment() {
Logd(TAG, "onDestroyView")
_binding = null
results.clear()
resultFeeds.clear()
vms.clear()
super.onDestroyView()
}
@ -157,7 +154,7 @@ class SearchFragment : Fragment() {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
@UnstableApi override fun onQueryTextSubmit(s: String): Boolean {
searchView.clearFocus()
searchWithProgressBar()
search()
return true
}
@UnstableApi override fun onQueryTextChange(s: String): Boolean {
@ -181,24 +178,24 @@ class SearchFragment : Fragment() {
})
}
override fun onContextItemSelected(item: MenuItem): Boolean {
val selectedFeedItem: Feed? = adapterFeeds.longPressedItem
if (selectedFeedItem != null && onMenuItemClicked(this, item.itemId, selectedFeedItem) {}) return true
return super.onContextItemSelected(item)
}
// override fun onContextItemSelected(item: MenuItem): Boolean {
//// val selectedFeedItem: Feed? = adapterFeeds.longPressedItem
//// if (selectedFeedItem != null && onMenuItemClicked(this, item.itemId, selectedFeedItem) {}) return true
// return super.onContextItemSelected(item)
// }
private fun onMenuItemClicked(fragment: Fragment, menuItemId: Int, selectedFeed: Feed, callback: Runnable): Boolean {
val context = fragment.requireContext()
when (menuItemId) {
// R.id.rename_folder_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
R.id.edit_tags -> if (selectedFeed.preferences != null) TagSettingsDialog.newInstance(listOf(selectedFeed))
.show(fragment.childFragmentManager, TagSettingsDialog.TAG)
R.id.rename_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
R.id.remove_feed -> RemoveFeedDialog.show(context, selectedFeed, null)
else -> return false
}
return true
}
// private fun onMenuItemClicked(fragment: Fragment, menuItemId: Int, selectedFeed: Feed, callback: Runnable): Boolean {
// val context = fragment.requireContext()
// when (menuItemId) {
//// R.id.rename_folder_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
// R.id.edit_tags -> if (selectedFeed.preferences != null) TagSettingsDialog.newInstance(listOf(selectedFeed))
// .show(fragment.childFragmentManager, TagSettingsDialog.TAG)
// R.id.rename_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
// R.id.remove_feed -> RemoveFeedDialog.show(context, selectedFeed, null)
// else -> return false
// }
// return true
// }
private var eventSink: Job? = null
private var eventStickySink: Job? = null
@ -240,14 +237,9 @@ class SearchFragment : Fragment() {
}
}
@UnstableApi private fun searchWithProgressBar() {
emptyViewHandler.hide()
search()
}
@SuppressLint("StringFormatMatches")
@UnstableApi private fun search() {
adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
// adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
lifecycleScope.launch {
@ -262,15 +254,43 @@ class SearchFragment : Fragment() {
for (e in first_) { vms.add(EpisodeVM(e)) }
}
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
if (results_.second != null) adapterFeeds.updateData(results_.second!!)
} else adapterFeeds.updateData(emptyList())
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
else emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.query))
if (results_.second != null) {
resultFeeds.clear()
resultFeeds.addAll(results_.second!!)
}
} else resultFeeds.clear()
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
}
}
enum class SearchBy(val nameRes: Int, var selected: Boolean = true) {
TITLE(R.string.title),
AUTHOR(R.string.author),
DESCRIPTION(R.string.description_label),
COMMENT(R.string.my_opinion_label),
}
@Composable
fun CriteriaList() {
val textColor = MaterialTheme.colorScheme.onSurface
NonlazyGrid(columns = 2, itemCount = SearchBy.entries.size) { index ->
val c = SearchBy.entries[index]
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
var isChecked by remember { mutableStateOf(true) }
Checkbox(
checked = isChecked,
onCheckedChange = { newValue ->
c.selected = newValue
isChecked = newValue
}
)
Spacer(modifier = Modifier.width(2.dp))
Text(stringResource(c.nameRes), color = textColor)
}
}
}
@UnstableApi private fun performSearch(): Pair<List<Episode>, List<Feed>> {
val query = searchView.query.toString()
if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList())
@ -282,45 +302,41 @@ class SearchFragment : Fragment() {
return Pair<List<Episode>, List<Feed>>(items, feeds)
}
private fun prepareFeedQueryString(query: String): String {
private fun searchFeeds(query: String): List<Feed> {
Logd(TAG, "searchFeeds called ${SearchBy.AUTHOR.selected}")
val queryWords = query.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val sb = StringBuilder()
for (i in queryWords.indices) {
sb.append("(")
.append("eigenTitle TEXT '${queryWords[i]}'")
.append(" OR ")
.append("customTitle TEXT '${queryWords[i]}'")
.append(" OR ")
.append("author TEXT '${queryWords[i]}'")
.append(" OR ")
.append("description TEXT '${queryWords[i]}'")
.append(") ")
var isStart = true
if (SearchBy.TITLE.selected) {
sb.append("eigenTitle TEXT '${queryWords[i]}'")
sb.append(" OR ")
sb.append("customTitle TEXT '${queryWords[i]}'")
isStart = false
}
if (SearchBy.AUTHOR.selected) {
if (!isStart) sb.append(" OR ")
sb.append("author TEXT '${queryWords[i]}'")
isStart = false
}
if (SearchBy.DESCRIPTION.selected) {
if (!isStart) sb.append(" OR ")
sb.append("description TEXT '${queryWords[i]}'")
isStart = false
}
if (SearchBy.COMMENT.selected) {
if (!isStart) sb.append(" OR ")
sb.append("comment TEXT '${queryWords[i]}'")
}
sb.append(") ")
if (i != queryWords.size - 1) sb.append("AND ")
}
return sb.toString()
}
private fun searchFeeds(query: String): List<Feed> {
Logd(TAG, "searchFeeds called")
val queryString = prepareFeedQueryString(query)
val queryString = sb.toString()
Logd(TAG, "searchFeeds queryString: $queryString")
return realm.query(Feed::class).query(queryString).find()
}
private fun prepareEpisodeQueryString(query: String): String {
val queryWords = query.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val sb = StringBuilder()
for (i in queryWords.indices) {
sb.append("(")
.append("description TEXT '${queryWords[i]}'")
.append(" OR ")
.append("title TEXT '${queryWords[i]}'" )
.append(") ")
if (i != queryWords.size - 1) sb.append("AND ")
}
return sb.toString()
}
/**
* Searches the FeedItems of a specific Feed for a given string.
* @param feedID The id of the feed whose episodes should be searched.
@ -330,7 +346,30 @@ class SearchFragment : Fragment() {
*/
private fun searchEpisodes(feedID: Long, query: String): List<Episode> {
Logd(TAG, "searchEpisodes called")
var queryString = prepareEpisodeQueryString(query)
val queryWords = query.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val sb = StringBuilder()
for (i in queryWords.indices) {
sb.append("(")
var isStart = true
if (SearchBy.TITLE.selected) {
sb.append("title TEXT '${queryWords[i]}'" )
isStart = false
}
if (SearchBy.DESCRIPTION.selected) {
if (!isStart) sb.append(" OR ")
sb.append("description TEXT '${queryWords[i]}'")
sb.append(" OR ")
sb.append("transcript TEXT '${queryWords[i]}'")
isStart = false
}
if (SearchBy.COMMENT.selected) {
if (!isStart) sb.append(" OR ")
sb.append("comment TEXT '${queryWords[i]}'")
}
sb.append(") ")
if (i != queryWords.size - 1) sb.append("AND ")
}
var queryString = sb.toString()
if (feedID != 0L) queryString = "(feedId == $feedID) AND $queryString"
Logd(TAG, "searchEpisodes queryString: $queryString")
return realm.query(Episode::class).query(queryString).find()
@ -354,89 +393,49 @@ class SearchFragment : Fragment() {
(activity as MainActivity).loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
}
open class HorizontalFeedListAdapter(mainActivity: MainActivity)
: RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private val data: MutableList<Feed> = ArrayList()
private var dummyViews = 0
var longPressedItem: Feed? = null
@StringRes
private var endButtonText = 0
private var endButtonAction: Runnable? = null
fun updateData(newData: List<Feed>?) {
data.clear()
data.addAll(newData!!)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null)
return Holder(convertView)
}
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
if (position == itemCount - 1 && endButtonAction != null) {
holder.cardView.visibility = View.GONE
holder.actionButton.visibility = View.VISIBLE
holder.actionButton.setText(endButtonText)
holder.actionButton.setOnClickListener { endButtonAction!!.run() }
return
}
holder.cardView.visibility = View.VISIBLE
holder.actionButton.visibility = View.GONE
if (position >= data.size) {
holder.itemView.alpha = 0.1f
// Glide.with(mainActivityRef.get()!!).clear(holder.imageView)
val imageLoader = ImageLoader.Builder(mainActivityRef.get()!!).build()
imageLoader.enqueue(ImageRequest.Builder(mainActivityRef.get()!!).data(null).target(holder.imageView).build())
holder.imageView.setImageResource(R.color.medium_gray)
return
}
holder.itemView.alpha = 1.0f
val podcast: Feed = data[position]
holder.imageView.setContentDescription(podcast.title)
holder.imageView.setOnClickListener {
mainActivityRef.get()?.loadChildFragment(FeedEpisodesFragment.newInstance(podcast.id))
}
holder.imageView.setOnCreateContextMenuListener(this)
holder.imageView.setOnLongClickListener {
val currentItemPosition = holder.bindingAdapterPosition
longPressedItem = data[currentItemPosition]
false
}
holder.imageView.load(podcast.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
}
override fun getItemId(position: Int): Long {
if (position >= data.size) return RecyclerView.NO_ID // Dummy views
return data[position].id
}
override fun getItemCount(): Int {
return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1)
}
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
if (longPressedItem == null) return
inflater.inflate(R.menu.feed_context, contextMenu)
contextMenu.setHeaderTitle(longPressedItem!!.title)
}
fun setEndButton(@StringRes text: Int, action: Runnable?) {
endButtonAction = action
endButtonText = text
notifyDataSetChanged()
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = HorizontalFeedItemBinding.bind(itemView)
var imageView: SquareImageView = binding.discoveryCover
var cardView: CardView
var actionButton: Button
init {
imageView.setDirection(SquareImageView.DIRECTION_HEIGHT)
actionButton = binding.actionButton
cardView = binding.cardView
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FeedsRow() {
val context = LocalContext.current
val lazyGridState = rememberLazyListState()
LazyRow (state = lazyGridState, horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)
) {
items(resultFeeds.size, key = {index -> resultFeeds[index].id}) { index ->
val feed by remember { mutableStateOf(resultFeeds[index]) }
ConstraintLayout {
val (coverImage, episodeCount, rating, error) = createRefs()
val imgLoc = remember(feed) { feed.imageUrl }
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "coverImage",
modifier = Modifier.height(100.dp).aspectRatio(1f)
.constrainAs(coverImage) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}.combinedClickable(onClick = {
Logd(SubscriptionsFragment.TAG, "clicked: ${feed.title}")
(activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
}, onLongClick = {
Logd(SubscriptionsFragment.TAG, "long clicked: ${feed.title}")
// val inflater: MenuInflater = (activity as MainActivity).menuInflater
// inflater.inflate(R.menu.feed_context, contextMenu)
// contextMenu.setHeaderTitle(feed.title)
})
)
Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()), color = Color.Green,
modifier = Modifier.background(Color.Gray).constrainAs(episodeCount) {
end.linkTo(parent.end)
top.linkTo(coverImage.top)
})
if (feed.rating != Rating.UNRATED.code)
Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating",
modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) {
start.linkTo(parent.start)
centerVerticallyTo(coverImage)
})
}
}
}
}

View File

@ -285,7 +285,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
is FlowEvent.FeedsFilterEvent -> loadSubscriptions()
// is FlowEvent.FeedsFilterEvent -> loadSubscriptions()
is FlowEvent.EpisodePlayedEvent -> loadSubscriptions()
is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions()
is FlowEvent.FeedPrefsChangeEvent -> loadSubscriptions()
@ -1089,7 +1089,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun onFilterChanged(newFilterValues: Set<String>) {
feedsFilter = StringUtils.join(newFilterValues, ",")
Logd(TAG, "onFilterChanged: $feedsFilter")
EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
loadSubscriptions()
// EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
}
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
@ -1097,7 +1098,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().height(500.dp), shape = RoundedCornerShape(16.dp)) {
Surface(modifier = Modifier.fillMaxWidth().height(350.dp), color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
@ -1109,8 +1110,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (selectNone) selectedIndex = -1
LaunchedEffect(Unit) {
if (filter != null) {
if (item.values[0].filterId in filter!!.properties) selectedIndex = 0
else if (item.values[1].filterId in filter!!.properties) selectedIndex = 1
if (item.values[0].filterId in filter.properties) selectedIndex = 0
else if (item.values[1].filterId in filter.properties) selectedIndex = 1
}
}
Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.padding(end = 10.dp))
@ -1231,137 +1232,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
// class FeedFilterDialog : BottomSheetDialogFragment() {
// var filter: FeedFilter? = null
// private val filterValues: MutableSet<String> = mutableSetOf()
//
// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// val composeView = ComposeView(requireContext()).apply {
// setContent {
// CustomTheme(requireContext()) {
// MainView()
// }
// }
// }
// return composeView
// }
//
// @Composable
// fun MainView() {
// val textColor = MaterialTheme.colorScheme.onSurface
// val scrollState = rememberScrollState()
// Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
// var selectNone by remember { mutableStateOf(false) }
// for (item in FeedFilter.FeedFilterGroup.entries) {
// if (item.values.size == 2) {
// Row(modifier = Modifier.padding(start = 5.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Absolute.Left, verticalAlignment = Alignment.CenterVertically) {
// var selectedIndex by remember { mutableStateOf(-1) }
// if (selectNone) selectedIndex = -1
// LaunchedEffect(Unit) {
// if (filter != null) {
// if (item.values[0].filterId in filter!!.properties) selectedIndex = 0
// else if (item.values[1].filterId in filter!!.properties) selectedIndex = 1
// }
// }
// Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.padding(end = 10.dp))
// Spacer(Modifier.weight(0.3f))
// OutlinedButton(
// modifier = Modifier.padding(2.dp).heightIn(min = 20.dp).widthIn(min = 20.dp), border = BorderStroke(2.dp, if (selectedIndex != 0) textColor else Color.Green),
// onClick = {
// if (selectedIndex != 0) {
// selectNone = false
// selectedIndex = 0
// filterValues.add(item.values[0].filterId)
// filterValues.remove(item.values[1].filterId)
// } else {
// selectedIndex = -1
// filterValues.remove(item.values[0].filterId)
// }
// onFilterChanged(filterValues)
// },
// ) {
// Text(text = stringResource(item.values[0].displayName), color = textColor)
// }
// Spacer(Modifier.weight(0.1f))
// OutlinedButton(
// modifier = Modifier.padding(2.dp).heightIn(min = 20.dp).widthIn(min = 20.dp), border = BorderStroke(2.dp, if (selectedIndex != 1) textColor else Color.Green),
// onClick = {
// if (selectedIndex != 1) {
// selectNone = false
// selectedIndex = 1
// filterValues.add(item.values[1].filterId)
// filterValues.remove(item.values[0].filterId)
// } else {
// selectedIndex = -1
// filterValues.remove(item.values[1].filterId)
// }
// onFilterChanged(filterValues)
// },
// ) {
// Text(text = stringResource(item.values[1].displayName), color = textColor)
// }
// Spacer(Modifier.weight(0.5f))
// }
// } else {
// Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
// Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor)
// val lazyGridState = rememberLazyGridState()
// LazyVerticalGrid(state = lazyGridState, columns = GridCells.Adaptive(100.dp),
// verticalArrangement = Arrangement.spacedBy(2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp)) {
// items(item.values.size) { index ->
// var selected by remember { mutableStateOf(false) }
// if (selectNone) selected = false
// OutlinedButton(
// modifier = Modifier.padding(2.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(),
// border = BorderStroke(2.dp, if (selected) Color.Green else textColor),
// onClick = {
// selectNone = false
// selected = !selected
// if (selected) filterValues.add(item.values[index].filterId)
// else filterValues.remove(item.values[index].filterId)
// onFilterChanged(filterValues)
// },
// ) {
// Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor)
// }
// }
// }
// }
// }
// }
// Row {
// Spacer(Modifier.weight(0.3f))
// Button(onClick = {
// selectNone = true
// onFilterChanged(setOf(""))
// }) {
// Text(stringResource(R.string.reset))
// }
// Spacer(Modifier.weight(0.4f))
// Button(onClick = {
// dismiss()
// }) {
// Text(stringResource(R.string.close_label))
// }
// Spacer(Modifier.weight(0.3f))
// }
// }
// }
// private fun onFilterChanged(newFilterValues: Set<String>) {
// feedsFilter = StringUtils.join(newFilterValues, ",")
// Logd(TAG, "onFilterChanged: $feedsFilter")
// EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
// }
//
// companion object {
// fun newInstance(filter: FeedFilter?): FeedFilterDialog {
// val dialog = FeedFilterDialog()
// dialog.filter = filter
// return dialog
// }
// }
// }
companion object {
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous"

View File

@ -141,7 +141,7 @@ sealed class FlowEvent {
data class FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent()
data class FeedsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
// data class FeedsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
// data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent()
@ -167,11 +167,11 @@ sealed class FlowEvent {
data class RatingEvent(val episode: Episode, val rating: Int = Rating.FAVORITE.code) : FlowEvent()
data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
// data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent()
data class DownloadsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
// data class DownloadsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
data class EpisodeEvent(val episodes: List<Episode>) : FlowEvent() {
companion object {

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/ac.mdiq.podcini"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="96dp"
android:orientation="vertical"
android:id="@+id/horizontal_feed_item"
android:padding="4dp"
android:clipToPadding="false"
android:clipToOutline="false"
android:clipChildren="false">
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/non_square_icon_background"
app:cardCornerRadius="16dp"
app:cardPreventCornerOverlap="false"
app:cardElevation="2dp">
<ac.mdiq.podcini.ui.view.SquareImageView
android:id="@+id/discovery_cover"
android:layout_width="match_parent"
android:layout_height="96dp"
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"
android:background="?android:attr/colorBackground"
squareImageView:direction="height" />
</androidx.cardview.widget.CardView>
<Button
android:id="@+id/actionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
style="@style/Widget.Material3.Button.OutlinedButton" />
</LinearLayout>

View File

@ -27,16 +27,8 @@
android:visibility="gone"
app:closeIconVisible="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewFeeds"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:clipToPadding="false" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/lazyColumn"
android:id="@+id/resultsListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View File

@ -9,12 +9,12 @@
custom:showAsAction="always"
android:title="@string/search_label"/>
<item
android:id="@+id/action_favorites"
android:icon="@drawable/ic_star_border"
android:menuCategory="container"
android:title="@string/favorite_episodes_label"
custom:showAsAction="always"/>
<!-- <item-->
<!-- android:id="@+id/action_favorites"-->
<!-- android:icon="@drawable/ic_star_border"-->
<!-- android:menuCategory="container"-->
<!-- android:title="@string/favorite_episodes_label"-->
<!-- custom:showAsAction="always"/>-->
<item
android:id="@+id/filter_items"

View File

@ -419,6 +419,8 @@
<string name="duration">Duration</string>
<string name="episode_title">Episode title</string>
<string name="feed_title">Podcast title</string>
<string name="title">Title</string>
<string name="author">Author</string>
<string name="random">Random</string>
<string name="smart_shuffle">Smart shuffle</string>
<string name="size">Size</string>

View File

@ -1,3 +1,9 @@
# 6.12.4
* bug fixes and enhancements in filters routines
* in SearchFragment, added search criteria options: title, author(feed only), description(including transcript in episodes), and comment (My opinion)
* feed list in SearchFragment is in Compose
# 6.12.3
* reworked and expanded the filters routines for episodes and feeds

View File

@ -0,0 +1,5 @@
Version 6.12.4
* bug fixes and enhancements in filters routines
* in SearchFragment, added search criteria options: title, author(feed only), description(including transcript in episodes), and comment (My opinion)
* feed list in SearchFragment is in Compose