From 15bc1efdcac1067e81c60d2dac24553e1775399c Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:45:39 +0100 Subject: [PATCH] 6.12.4 commit --- app/build.gradle | 4 +- .../ac/mdiq/podcini/storage/model/Episode.kt | 1 + .../podcini/storage/model/EpisodeFilter.kt | 41 +- .../ac/mdiq/podcini/storage/model/Feed.kt | 2 + .../mdiq/podcini/storage/model/FeedFilter.kt | 57 +-- .../mdiq/podcini/storage/model/MediaType.kt | 6 - .../podcini/ui/activity/OpmlImportActivity.kt | 5 +- .../ac/mdiq/podcini/ui/compose/EpisodesVM.kt | 2 +- .../podcini/ui/dialog/EpisodeFilterDialog.kt | 147 ------- .../ui/fragment/AllEpisodesFragment.kt | 31 +- .../podcini/ui/fragment/DownloadsFragment.kt | 38 +- .../podcini/ui/fragment/HistoryFragment.kt | 4 +- .../podcini/ui/fragment/OnlineFeedFragment.kt | 52 +-- .../podcini/ui/fragment/SearchFragment.kt | 363 +++++++++--------- .../ui/fragment/SubscriptionsFragment.kt | 142 +------ .../kotlin/ac/mdiq/podcini/util/FlowEvent.kt | 6 +- .../main/res/layout/horizontal_feed_item.xml | 43 --- app/src/main/res/layout/search_fragment.xml | 10 +- app/src/main/res/menu/episodes.xml | 12 +- app/src/main/res/values/strings.xml | 2 + changelog.md | 6 + .../android/en-US/changelogs/3020282.txt | 5 + 22 files changed, 303 insertions(+), 676 deletions(-) delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt delete mode 100644 app/src/main/res/layout/horizontal_feed_item.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020282.txt diff --git a/app/build.gradle b/app/build.gradle index ee142fe6..0d617aa1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt index 750aebdd..1f7c0d3e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt @@ -89,6 +89,7 @@ class Episode : RealmObject { var isFavorite: Boolean = (rating == Rating.FAVORITE.code) private set + @FullText var comment: String = "" @Ignore diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt index c2c87a37..a85fae63 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt @@ -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 = arrayOf(*values) + val values: Array = arrayOf(*values_) - class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String) + class ItemProperties(val displayName: Int, val filterId: String) } companion object { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 7f044050..4dbc9b91 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -49,6 +49,7 @@ class Feed : RealmObject { @FullText var author: String? = null + var imageUrl: String? = null var episodes: RealmList = realmListOf() @@ -98,6 +99,7 @@ class Feed : RealmObject { var rating: Int = Rating.NEUTRAL.code + @FullText var comment: String = "" /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt index e4b40277..234db64e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt @@ -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 = 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() @@ -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 = arrayOf(*values) + val values: Array = arrayOf(*values_) - class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String) + class ItemProperties(val displayName: Int, val filterId: String) } companion object { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/MediaType.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/MediaType.kt index 1187a2ac..83321eee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/MediaType.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/MediaType.kt @@ -4,12 +4,6 @@ enum class MediaType { AUDIO, VIDEO, UNKNOWN; companion object { -// private val AUDIO_APPLICATION_MIME_STRINGS: Set = HashSet(mutableListOf( -// "application/ogg", -// "application/opus", -// "application/x-flac" -// )) - val AUDIO_APPLICATION_MIME_STRINGS: HashSet = hashSetOf( "application/ogg", "application/opus", diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt index 7a89f32a..fe359d38 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index 6ded64d8..19264d18 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -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)) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt deleted file mode 100644 index e9a0050f..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt +++ /dev/null @@ -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 = mutableSetOf() - private val filterValues: MutableSet = 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) - -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index a969a70f..90c7d198 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -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) { - EventFlow.postEvent(FlowEvent.AllEpisodesFilterEvent(filterValues)) +// EventFlow.postEvent(FlowEvent.AllEpisodesFilterEvent(filterValues)) + prefFilterAllEpisodes = StringUtils.join(filterValues, ",") + page = 1 + loadItems() } class AllEpisodesSortDialog : EpisodeSortDialog() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 4c83942b..71056820 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -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) { -// 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" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index ec1a3087..4fc5d2c0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index 96636898..880bfe7e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -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" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 9b69f6b8..52711efe 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -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() private val results = mutableListOf() private val vms = mutableStateListOf() @@ -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> { val query = searchView.query.toString() if (query.isEmpty()) return Pair, List>(emptyList(), emptyList()) @@ -282,45 +302,41 @@ class SearchFragment : Fragment() { return Pair, List>(items, feeds) } - private fun prepareFeedQueryString(query: String): String { + private fun searchFeeds(query: String): List { + 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 { - 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 { 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(), View.OnCreateContextMenuListener { - - private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private val data: MutableList = ArrayList() - private var dummyViews = 0 - var longPressedItem: Feed? = null - @StringRes - private var endButtonText = 0 - private var endButtonAction: Runnable? = null - - fun updateData(newData: List?) { - 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) + }) + } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index ec6a0bc3..0824e530 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -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) { 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 = 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) { -// 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" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/FlowEvent.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/FlowEvent.kt index 0ecff408..d39a141d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/FlowEvent.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/FlowEvent.kt @@ -141,7 +141,7 @@ sealed class FlowEvent { data class FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent() - data class FeedsFilterEvent(val filterValues: Set?) : FlowEvent() +// data class FeedsFilterEvent(val filterValues: Set?) : 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?) : FlowEvent() +// data class AllEpisodesFilterEvent(val filterValues: Set?) : FlowEvent() data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent() - data class DownloadsFilterEvent(val filterValues: Set?) : FlowEvent() +// data class DownloadsFilterEvent(val filterValues: Set?) : FlowEvent() data class EpisodeEvent(val episodes: List) : FlowEvent() { companion object { diff --git a/app/src/main/res/layout/horizontal_feed_item.xml b/app/src/main/res/layout/horizontal_feed_item.xml deleted file mode 100644 index 22edf457..00000000 --- a/app/src/main/res/layout/horizontal_feed_item.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - -