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" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020281 versionCode 3020282
versionName "6.12.3" versionName "6.12.4"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

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

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
import ac.mdiq.podcini.storage.model.MediaType.Companion.AUDIO_APPLICATION_MIME_STRINGS
import java.io.Serializable import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable { class EpisodeFilter(vararg properties_: String) : Serializable {
@ -20,29 +19,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
constructor(properties: String) : this(*(properties.split(",").toTypedArray())) 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 // 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 { fun matchesForQueues(item: Episode): Boolean {
return when { 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.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.audio.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'audio' ")
if (properties.contains(States.video.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'video' ") 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()) { if (mediaTypeQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + mediaTypeQuerys[0]) 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(" OR ")
query.append(r) 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 (properties.contains(States.favorite.name)) ratingQuerys.add(" rating == ${Rating.FAVORITE.code} ")
if (ratingQuerys.isNotEmpty()) { if (ratingQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + ratingQuerys[0]) 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(" OR ")
query.append(r) 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 (properties.contains(States.ignored.name)) stateQuerys.add(" playState == ${PlayState.IGNORED.code} ")
if (stateQuerys.isNotEmpty()) { if (stateQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + stateQuerys[0]) 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(" OR ")
query.append(r) query.append(r)
} }
@ -198,11 +174,10 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
favorite, 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)), // 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)), // 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)), // 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), RATING(R.string.rating_label, ItemProperties(R.string.unrated, States.unrated.name),
ItemProperties(R.string.trash, States.trash.name), ItemProperties(R.string.trash, States.trash.name),
ItemProperties(R.string.bad, States.bad.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)), 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)), // 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)), 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)), 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), 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)); 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 { companion object {

View File

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

View File

@ -11,37 +11,17 @@ class FeedFilter(vararg properties_: String) : Serializable {
constructor(properties: String) : this(*(properties.split(",").toTypedArray())) 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 { fun queryString(): String {
val statements: MutableList<String> = mutableListOf() val statements: MutableList<String> = mutableListOf()
when { when {
properties.contains(States.keepUpdated.name) -> statements.add("preferences.keepUpdated == true ") properties.contains(States.keepUpdated.name) -> statements.add("preferences.keepUpdated == true ")
properties.contains(States.not_keepUpdated.name) -> statements.add(" preferences.keepUpdated == false ") 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 { when {
properties.contains(States.global_playSpeed.name) -> statements.add(" preferences.playSpeed == $SPEED_USE_GLOBAL ") 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 ") properties.contains(States.custom_playSpeed.name) -> statements.add(" preferences.playSpeed != $SPEED_USE_GLOBAL ")
@ -64,7 +44,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
} }
when { 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.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>() 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 (properties.contains(States.favorite.name)) ratingQuerys.add(" rating == ${Rating.FAVORITE.code} ")
if (ratingQuerys.isNotEmpty()) { if (ratingQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + ratingQuerys[0]) 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(" OR ")
query.append(r) 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 (properties.contains(States.never_auto_delete.name)) audoDeleteQuerys.add(" preferences.playSpeed == ${FeedPreferences.AutoDeleteAction.NEVER.code} ")
if (audoDeleteQuerys.isNotEmpty()) { if (audoDeleteQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + audoDeleteQuerys[0]) 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(" OR ")
query.append(r) query.append(r)
} }
@ -110,6 +90,7 @@ class FeedFilter(vararg properties_: String) : Serializable {
query.append(r) query.append(r)
} }
query.append(") ") query.append(") ")
Logd("queryString", "${query}")
return query.toString() return query.toString()
} }
@ -117,6 +98,8 @@ class FeedFilter(vararg properties_: String) : Serializable {
enum class States { enum class States {
keepUpdated, keepUpdated,
not_keepUpdated, not_keepUpdated,
pref_streaming,
not_pref_streaming,
global_playSpeed, global_playSpeed,
custom_playSpeed, custom_playSpeed,
has_skips, has_skips,
@ -142,14 +125,9 @@ class FeedFilter(vararg properties_: String) : Serializable {
favorite, 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)), 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)), 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), RATING(R.string.rating_label, ItemProperties(R.string.unrated, States.unrated.name),
ItemProperties(R.string.trash, States.trash.name), ItemProperties(R.string.trash, States.trash.name),
ItemProperties(R.string.bad, States.bad.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.good, States.good.name),
ItemProperties(R.string.favorite, States.favorite.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), 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.never, States.never_auto_delete.name),
ItemProperties(R.string.global, States.global_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)); 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 { companion object {

View File

@ -4,12 +4,6 @@ enum class MediaType {
AUDIO, VIDEO, UNKNOWN; AUDIO, VIDEO, UNKNOWN;
companion object { 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( val AUDIO_APPLICATION_MIME_STRINGS: HashSet<String> = hashSetOf(
"application/ogg", "application/ogg",
"application/opus", "application/opus",

View File

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

View File

@ -943,7 +943,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
window.setGravity(Gravity.BOTTOM) window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f) 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { 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 showFilterDialog = true
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) // AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
} }
R.id.action_favorites -> { // R.id.action_favorites -> {
val filter = getFilter().properties.toMutableSet() // val filter = getFilter().properties.toMutableSet()
if (filter.contains(EpisodeFilter.States.is_favorite.name)) filter.remove(EpisodeFilter.States.is_favorite.name) // if (filter.contains(EpisodeFilter.States.is_favorite.name)) filter.remove(EpisodeFilter.States.is_favorite.name)
else filter.add(EpisodeFilter.States.is_favorite.name) // else filter.add(EpisodeFilter.States.is_favorite.name)
onFilterChanged(FlowEvent.AllEpisodesFilterEvent(HashSet(filter))) // onFilterChanged(FlowEvent.AllEpisodesFilterEvent(HashSet(filter)))
} // }
R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show() R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
else -> return false else -> return false
@ -117,7 +117,7 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
EventFlow.events.collectLatest { event -> EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}") Logd(TAG, "Received event: ${event.TAG}")
when (event) { when (event) {
is FlowEvent.AllEpisodesFilterEvent -> onFilterChanged(event) // is FlowEvent.AllEpisodesFilterEvent -> onFilterChanged(event)
is FlowEvent.AllEpisodesSortEvent -> { is FlowEvent.AllEpisodesSortEvent -> {
page = 1 page = 1
loadItems() loadItems()
@ -128,11 +128,11 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
} }
} }
private fun onFilterChanged(event: FlowEvent.AllEpisodesFilterEvent) { // private fun onFilterChanged(event: FlowEvent.AllEpisodesFilterEvent) {
prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",") // prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",")
page = 1 // page = 1
loadItems() // loadItems()
} // }
override fun updateToolbar() { override fun updateToolbar() {
swipeActions.setFilter(getFilter()) swipeActions.setFilter(getFilter())
@ -142,11 +142,14 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
emptyView.setMessage(R.string.no_all_episodes_filtered_label) emptyView.setMessage(R.string.no_all_episodes_filtered_label)
} else emptyView.setMessage(R.string.no_all_episodes_label) } else emptyView.setMessage(R.string.no_all_episodes_label)
infoBarText.value = info 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>) { 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() { class AllEpisodesSortDialog : EpisodeSortDialog() {

View File

@ -100,7 +100,12 @@ import java.util.*
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(),
filtersDisabled = mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA), filtersDisabled = mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA),
onDismissRequest = { showFilterDialog = false } ) { 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 { Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
@ -261,7 +266,7 @@ import java.util.*
Logd(TAG, "Received event: ${event.TAG}") Logd(TAG, "Received event: ${event.TAG}")
when (event) { when (event) {
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event) is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event) // is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event)
is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event) is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event)
is FlowEvent.PlayerSettingsEvent -> loadItems() is FlowEvent.PlayerSettingsEvent -> loadItems()
is FlowEvent.DownloadLogEvent -> loadItems() is FlowEvent.DownloadLogEvent -> loadItems()
@ -283,13 +288,13 @@ import java.util.*
// } // }
} }
private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) { // private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) {
val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf() // val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf()
fSet.add(EpisodeFilter.States.downloaded.name) // fSet.add(EpisodeFilter.States.downloaded.name)
prefFilterDownloads = StringUtils.join(fSet, ",") // prefFilterDownloads = StringUtils.join(fSet, ",")
Logd(TAG, "onFilterChanged: $prefFilterDownloads") // Logd(TAG, "onFilterChanged: $prefFilterDownloads")
loadItems() // loadItems()
} // }
private fun addEmptyView() { private fun addEmptyView() {
emptyView = EmptyViewHandler(requireContext()) 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 { companion object {
val TAG = DownloadsFragment::class.simpleName ?: "Anonymous" 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 { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState) val root = super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
toolbar.inflateMenu(R.menu.playback_history) toolbar.inflateMenu(R.menu.playback_history)
toolbar.setTitle(R.string.playback_history_label) toolbar.setTitle(R.string.playback_history_label)
@ -50,7 +49,6 @@ import kotlin.math.min
emptyView.setIcon(R.drawable.ic_history) emptyView.setIcon(R.drawable.ic_history)
emptyView.setTitle(R.string.no_history_head_label) emptyView.setTitle(R.string.no_history_head_label)
emptyView.setMessage(R.string.no_history_label) emptyView.setMessage(R.string.no_history_label)
return root return root
} }
@ -130,7 +128,7 @@ import kotlin.math.min
emptyView.setMessage(R.string.no_all_episodes_filtered_label) emptyView.setMessage(R.string.no_all_episodes_filtered_label)
} else emptyView.setMessage(R.string.no_all_episodes_label) } else emptyView.setMessage(R.string.no_all_episodes_label)
infoBarText.value = info 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 private var eventSink: Job? = null

View File

@ -720,14 +720,14 @@ class OnlineFeedFragment : Fragment() {
updateToolbar() updateToolbar()
return root return root
} }
override fun onStart() { // override fun onStart() {
super.onStart() // super.onStart()
procFlowEvents() //// procFlowEvents()
} // }
override fun onStop() { // override fun onStop() {
super.onStop() // super.onStop()
cancelFlowEvents() //// cancelFlowEvents()
} // }
override fun onDestroyView() { override fun onDestroyView() {
episodeList.clear() episodeList.clear()
super.onDestroyView() 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.episodes_sort).setVisible(false)
// binding.toolbar.menu.findItem(R.id.refresh_item).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_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) binding.toolbar.menu.findItem(R.id.filter_items).setVisible(false)
infoBarText.value = "${episodes.size} episodes" infoBarText.value = "${episodes.size} episodes"
} }
@ -760,23 +760,23 @@ class OnlineFeedFragment : Fragment() {
else -> return false else -> return false
} }
} }
private var eventSink: Job? = null // private var eventSink: Job? = null
private fun cancelFlowEvents() { // private fun cancelFlowEvents() {
eventSink?.cancel() // eventSink?.cancel()
eventSink = null // eventSink = null
} // }
private fun procFlowEvents() { // private fun procFlowEvents() {
if (eventSink != null) return // if (eventSink != null) return
eventSink = lifecycleScope.launch { // eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event -> // EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}") // Logd(TAG, "Received event: ${event.TAG}")
when (event) { // when (event) {
is FlowEvent.AllEpisodesFilterEvent -> page = 1 // is FlowEvent.AllEpisodesFilterEvent -> page = 1
else -> {} // else -> {}
} // }
} // }
} // }
} // }
companion object { companion object {
const val PREF_NAME: String = "EpisodesListFragment" 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.R
import ac.mdiq.podcini.databinding.HorizontalFeedItemBinding
import ac.mdiq.podcini.databinding.SearchFragmentBinding import ac.mdiq.podcini.databinding.SearchFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed 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.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
import ac.mdiq.podcini.ui.compose.EpisodeVM import ac.mdiq.podcini.ui.compose.EpisodeVM
import ac.mdiq.podcini.ui.dialog.CustomFeedNameDialog import ac.mdiq.podcini.ui.compose.NonlazyGrid
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.util.EventFlow import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.Pair 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.view.inputmethod.InputMethodManager
import android.widget.Button
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.cardview.widget.CardView import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.mutableStateListOf 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.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager import coil.compose.AsyncImage
import androidx.recyclerview.widget.RecyclerView import coil.request.CachePolicy
import coil.ImageLoader
import coil.load
import coil.request.ImageRequest import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -53,7 +64,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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. * 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 var _binding: SearchFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var adapterFeeds: HorizontalFeedListAdapter
private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private lateinit var chip: Chip private lateinit var chip: Chip
private lateinit var automaticSearchDebouncer: Handler private lateinit var automaticSearchDebouncer: Handler
private val resultFeeds = mutableStateListOf<Feed>()
private val results = mutableListOf<Episode>() private val results = mutableListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>() private val vms = mutableStateListOf<EpisodeVM>()
@ -86,34 +96,20 @@ class SearchFragment : Fragment() {
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
setupToolbar(binding.toolbar) setupToolbar(binding.toolbar)
binding.lazyColumn.setContent { binding.resultsListView.setContent {
CustomTheme(requireContext()) { 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 = binding.feedTitleChip
chip.setOnCloseIconClickListener { chip.setOnCloseIconClickListener {
requireArguments().putLong(ARG_FEED, 0) requireArguments().putLong(ARG_FEED, 0)
searchWithProgressBar() search()
} }
chip.visibility = if (requireArguments().getLong(ARG_FEED, 0) == 0L) View.GONE else View.VISIBLE chip.visibility = if (requireArguments().getLong(ARG_FEED, 0) == 0L) View.GONE else View.VISIBLE
chip.text = requireArguments().getString(ARG_FEED_NAME, "") chip.text = requireArguments().getString(ARG_FEED_NAME, "")
@ -139,6 +135,7 @@ class SearchFragment : Fragment() {
Logd(TAG, "onDestroyView") Logd(TAG, "onDestroyView")
_binding = null _binding = null
results.clear() results.clear()
resultFeeds.clear()
vms.clear() vms.clear()
super.onDestroyView() super.onDestroyView()
} }
@ -157,7 +154,7 @@ class SearchFragment : Fragment() {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
@UnstableApi override fun onQueryTextSubmit(s: String): Boolean { @UnstableApi override fun onQueryTextSubmit(s: String): Boolean {
searchView.clearFocus() searchView.clearFocus()
searchWithProgressBar() search()
return true return true
} }
@UnstableApi override fun onQueryTextChange(s: String): Boolean { @UnstableApi override fun onQueryTextChange(s: String): Boolean {
@ -181,24 +178,24 @@ class SearchFragment : Fragment() {
}) })
} }
override fun onContextItemSelected(item: MenuItem): Boolean { // override fun onContextItemSelected(item: MenuItem): Boolean {
val selectedFeedItem: Feed? = adapterFeeds.longPressedItem //// val selectedFeedItem: Feed? = adapterFeeds.longPressedItem
if (selectedFeedItem != null && onMenuItemClicked(this, item.itemId, selectedFeedItem) {}) return true //// if (selectedFeedItem != null && onMenuItemClicked(this, item.itemId, selectedFeedItem) {}) return true
return super.onContextItemSelected(item) // return super.onContextItemSelected(item)
} // }
private fun onMenuItemClicked(fragment: Fragment, menuItemId: Int, selectedFeed: Feed, callback: Runnable): Boolean { // private fun onMenuItemClicked(fragment: Fragment, menuItemId: Int, selectedFeed: Feed, callback: Runnable): Boolean {
val context = fragment.requireContext() // val context = fragment.requireContext()
when (menuItemId) { // when (menuItemId) {
// R.id.rename_folder_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show() //// R.id.rename_folder_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
R.id.edit_tags -> if (selectedFeed.preferences != null) TagSettingsDialog.newInstance(listOf(selectedFeed)) // R.id.edit_tags -> if (selectedFeed.preferences != null) TagSettingsDialog.newInstance(listOf(selectedFeed))
.show(fragment.childFragmentManager, TagSettingsDialog.TAG) // .show(fragment.childFragmentManager, TagSettingsDialog.TAG)
R.id.rename_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show() // R.id.rename_item -> CustomFeedNameDialog(fragment.activity as Activity, selectedFeed).show()
R.id.remove_feed -> RemoveFeedDialog.show(context, selectedFeed, null) // R.id.remove_feed -> RemoveFeedDialog.show(context, selectedFeed, null)
else -> return false // else -> return false
} // }
return true // return true
} // }
private var eventSink: Job? = null private var eventSink: Job? = null
private var eventStickySink: Job? = null private var eventStickySink: Job? = null
@ -240,14 +237,9 @@ class SearchFragment : Fragment() {
} }
} }
@UnstableApi private fun searchWithProgressBar() {
emptyViewHandler.hide()
search()
}
@SuppressLint("StringFormatMatches") @SuppressLint("StringFormatMatches")
@UnstableApi private fun search() { @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 chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
lifecycleScope.launch { lifecycleScope.launch {
@ -262,15 +254,43 @@ class SearchFragment : Fragment() {
for (e in first_) { vms.add(EpisodeVM(e)) } for (e in first_) { vms.add(EpisodeVM(e)) }
} }
if (requireArguments().getLong(ARG_FEED, 0) == 0L) { if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
if (results_.second != null) adapterFeeds.updateData(results_.second!!) if (results_.second != null) {
} else adapterFeeds.updateData(emptyList()) resultFeeds.clear()
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search) resultFeeds.addAll(results_.second!!)
else emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.query)) }
} else resultFeeds.clear()
} }
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } 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>> { @UnstableApi private fun performSearch(): Pair<List<Episode>, List<Feed>> {
val query = searchView.query.toString() val query = searchView.query.toString()
if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList()) 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) 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 queryWords = query.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val sb = StringBuilder() val sb = StringBuilder()
for (i in queryWords.indices) { for (i in queryWords.indices) {
sb.append("(") sb.append("(")
.append("eigenTitle TEXT '${queryWords[i]}'") var isStart = true
.append(" OR ") if (SearchBy.TITLE.selected) {
.append("customTitle TEXT '${queryWords[i]}'") sb.append("eigenTitle TEXT '${queryWords[i]}'")
.append(" OR ") sb.append(" OR ")
.append("author TEXT '${queryWords[i]}'") sb.append("customTitle TEXT '${queryWords[i]}'")
.append(" OR ") isStart = false
.append("description TEXT '${queryWords[i]}'") }
.append(") ") 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 ") if (i != queryWords.size - 1) sb.append("AND ")
} }
return sb.toString() val queryString = sb.toString()
}
private fun searchFeeds(query: String): List<Feed> {
Logd(TAG, "searchFeeds called")
val queryString = prepareFeedQueryString(query)
Logd(TAG, "searchFeeds queryString: $queryString") Logd(TAG, "searchFeeds queryString: $queryString")
return realm.query(Feed::class).query(queryString).find() 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. * Searches the FeedItems of a specific Feed for a given string.
* @param feedID The id of the feed whose episodes should be searched. * @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> { private fun searchEpisodes(feedID: Long, query: String): List<Episode> {
Logd(TAG, "searchEpisodes called") 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" if (feedID != 0L) queryString = "(feedId == $feedID) AND $queryString"
Logd(TAG, "searchEpisodes queryString: $queryString") Logd(TAG, "searchEpisodes queryString: $queryString")
return realm.query(Episode::class).query(queryString).find() 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)) (activity as MainActivity).loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
} }
open class HorizontalFeedListAdapter(mainActivity: MainActivity) @OptIn(ExperimentalFoundationApi::class)
: RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener { @Composable
fun FeedsRow() {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity) val context = LocalContext.current
private val data: MutableList<Feed> = ArrayList() val lazyGridState = rememberLazyListState()
private var dummyViews = 0 LazyRow (state = lazyGridState, horizontalArrangement = Arrangement.spacedBy(16.dp),
var longPressedItem: Feed? = null contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)
@StringRes ) {
private var endButtonText = 0 items(resultFeeds.size, key = {index -> resultFeeds[index].id}) { index ->
private var endButtonAction: Runnable? = null val feed by remember { mutableStateOf(resultFeeds[index]) }
ConstraintLayout {
fun updateData(newData: List<Feed>?) { val (coverImage, episodeCount, rating, error) = createRefs()
data.clear() val imgLoc = remember(feed) { feed.imageUrl }
data.addAll(newData!!) AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
notifyDataSetChanged() .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
} contentDescription = "coverImage",
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { modifier = Modifier.height(100.dp).aspectRatio(1f)
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null) .constrainAs(coverImage) {
return Holder(convertView) top.linkTo(parent.top)
} bottom.linkTo(parent.bottom)
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) { start.linkTo(parent.start)
if (position == itemCount - 1 && endButtonAction != null) { }.combinedClickable(onClick = {
holder.cardView.visibility = View.GONE Logd(SubscriptionsFragment.TAG, "clicked: ${feed.title}")
holder.actionButton.visibility = View.VISIBLE (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
holder.actionButton.setText(endButtonText) }, onLongClick = {
holder.actionButton.setOnClickListener { endButtonAction!!.run() } Logd(SubscriptionsFragment.TAG, "long clicked: ${feed.title}")
return // val inflater: MenuInflater = (activity as MainActivity).menuInflater
} // inflater.inflate(R.menu.feed_context, contextMenu)
holder.cardView.visibility = View.VISIBLE // contextMenu.setHeaderTitle(feed.title)
holder.actionButton.visibility = View.GONE })
if (position >= data.size) { )
holder.itemView.alpha = 0.1f Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()), color = Color.Green,
// Glide.with(mainActivityRef.get()!!).clear(holder.imageView) modifier = Modifier.background(Color.Gray).constrainAs(episodeCount) {
val imageLoader = ImageLoader.Builder(mainActivityRef.get()!!).build() end.linkTo(parent.end)
imageLoader.enqueue(ImageRequest.Builder(mainActivityRef.get()!!).data(null).target(holder.imageView).build()) top.linkTo(coverImage.top)
holder.imageView.setImageResource(R.color.medium_gray) })
return if (feed.rating != Rating.UNRATED.code)
} Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating",
holder.itemView.alpha = 1.0f modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) {
val podcast: Feed = data[position] start.linkTo(parent.start)
holder.imageView.setContentDescription(podcast.title) centerVerticallyTo(coverImage)
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
} }
} }
} }

View File

@ -285,7 +285,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "Received event: ${event.TAG}") Logd(TAG, "Received event: ${event.TAG}")
when (event) { when (event) {
is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions() is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
is FlowEvent.FeedsFilterEvent -> loadSubscriptions() // is FlowEvent.FeedsFilterEvent -> loadSubscriptions()
is FlowEvent.EpisodePlayedEvent -> loadSubscriptions() is FlowEvent.EpisodePlayedEvent -> loadSubscriptions()
is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions() is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions()
is FlowEvent.FeedPrefsChangeEvent -> loadSubscriptions() is FlowEvent.FeedPrefsChangeEvent -> loadSubscriptions()
@ -1089,7 +1089,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun onFilterChanged(newFilterValues: Set<String>) { fun onFilterChanged(newFilterValues: Set<String>) {
feedsFilter = StringUtils.join(newFilterValues, ",") feedsFilter = StringUtils.join(newFilterValues, ",")
Logd(TAG, "onFilterChanged: $feedsFilter") Logd(TAG, "onFilterChanged: $feedsFilter")
EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues)) loadSubscriptions()
// EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
} }
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) { Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
@ -1097,7 +1098,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
window.setGravity(Gravity.BOTTOM) window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f) 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
@ -1109,8 +1110,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (selectNone) selectedIndex = -1 if (selectNone) selectedIndex = -1
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (filter != null) { if (filter != null) {
if (item.values[0].filterId in filter!!.properties) selectedIndex = 0 if (item.values[0].filterId in filter.properties) selectedIndex = 0
else if (item.values[1].filterId in filter!!.properties) selectedIndex = 1 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)) 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 { companion object {
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous" 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 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() // 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 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 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() { data class EpisodeEvent(val episodes: List<Episode>) : FlowEvent() {
companion object { 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" android:visibility="gone"
app:closeIconVisible="true" /> 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 <androidx.compose.ui.platform.ComposeView
android:id="@+id/lazyColumn" android:id="@+id/resultsListView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>

View File

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

View File

@ -419,6 +419,8 @@
<string name="duration">Duration</string> <string name="duration">Duration</string>
<string name="episode_title">Episode title</string> <string name="episode_title">Episode title</string>
<string name="feed_title">Podcast 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="random">Random</string>
<string name="smart_shuffle">Smart shuffle</string> <string name="smart_shuffle">Smart shuffle</string>
<string name="size">Size</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 # 6.12.3
* reworked and expanded the filters routines for episodes and feeds * 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