From 50e9dffdb8265439b164819c9c4550e4d71d46fb Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:51:54 +0100 Subject: [PATCH] 6.3.5 commit --- app/build.gradle | 4 +- .../service/DownloadServiceInterfaceImpl.kt | 1 - .../playback/service/PlaybackService.kt | 2 +- .../fragments/MainPreferencesFragment.kt | 2 +- .../podcini/receiver/MediaButtonReceiver.kt | 31 +- .../storage/algorithms/AutoDownloads.kt | 43 +- .../ac/mdiq/podcini/storage/database/Feeds.kt | 5 + .../mdiq/podcini/storage/database/Queues.kt | 5 - .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../podcini/storage/model/FeedPreferences.kt | 12 +- .../storage/model/VolumeAdaptionSetting.kt | 16 +- .../ui/actions/swipeactions/SwipeActions.kt | 10 +- .../podcini/ui/activity/PreferenceActivity.kt | 20 +- .../podcini/ui/dialog/SkipPreferenceDialog.kt | 8 +- .../podcini/ui/dialog/TagSettingsDialog.kt | 9 +- .../ui/fragment/FeedSettingsFragment.kt | 967 +++++++++++------- .../podcini/ui/fragment/NavDrawerFragment.kt | 10 +- .../podcini/ui/fragment/QueuesFragment.kt | 30 +- .../ui/fragment/SubscriptionsFragment.kt | 16 +- .../ac/mdiq/podcini/util/DateFormatter.kt | 34 +- app/src/main/res/layout/feedsettings.xml | 19 +- app/src/main/res/values/arrays.xml | 64 -- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/feed_settings.xml | 89 -- changelog.md | 8 + 25 files changed, 757 insertions(+), 652 deletions(-) delete mode 100644 app/src/main/res/xml/feed_settings.xml diff --git a/app/build.gradle b/app/build.gradle index 508357db..8946cf8c 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 3020228 - versionName "6.3.4" + versionCode 3020229 + versionName "6.3.5" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index da1967d1..9e91f219 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -85,7 +85,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { try { val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result workInfoList.forEach { workInfo -> -// TODO: why cancel so many times?? if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { val item_ = media.episodeOrFetch() if (item_ != null) Queues.removeFromQueue(item_) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index f46f6232..29066f18 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -271,7 +271,7 @@ class PlaybackService : MediaSessionService() { } override fun onMediaChanged(reloadUI: Boolean) { - Logd(TAG, "reloadUI callback reached") + Logd(TAG, "onMediaChanged reloadUI callback reached") if (reloadUI) sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt index 6765c45c..a4b55dea 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt @@ -138,7 +138,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { .addBreadcrumb(getTitleOfPage(R.xml.preferences_autodownload)) config.index(R.xml.preferences_synchronization).addBreadcrumb(getTitleOfPage(R.xml.preferences_synchronization)) config.index(R.xml.preferences_notifications).addBreadcrumb(getTitleOfPage(R.xml.preferences_notifications)) - config.index(R.xml.feed_settings).addBreadcrumb(getTitleOfPage(R.xml.feed_settings)) +// config.index(R.xml.feed_settings).addBreadcrumb(getTitleOfPage(R.xml.feed_settings)) config.index(R.xml.preferences_swipe) .addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface)) .addBreadcrumb(getTitleOfPage(R.xml.preferences_swipe)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt index 48b8736b..6961110d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt @@ -1,8 +1,6 @@ package ac.mdiq.podcini.receiver -import ac.mdiq.podcini.playback.service.PlaybackService -import ac.mdiq.podcini.playback.service.PlaybackService.Companion -import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.util.config.ClientConfigurator import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context @@ -12,7 +10,6 @@ import android.util.Log import android.view.KeyEvent import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.util.config.ClientConfigurator /** * Receives media button events. @@ -20,17 +17,29 @@ import ac.mdiq.podcini.util.config.ClientConfigurator class MediaButtonReceiver : BroadcastReceiver() { @UnstableApi override fun onReceive(context: Context, intent: Intent) { - Log.d(TAG, "onReceive called with action: ${intent.action}") - if (intent.extras == null) return + Log.d(TAG, "onReceive Received intent: $intent") + Log.d(TAG, "onReceive Action: ${intent.action}") + val extras = intent.extras + Log.d(TAG, "onReceive Extras: $extras") + if (extras == null) return - val event = intent.extras!![Intent.EXTRA_KEY_EVENT] as? KeyEvent - if (event != null && event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { + Log.d(TAG, "onReceive Extras: ${extras.keySet()}") + for (key in extras.keySet()) { + Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}") + } + +// val event = extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) + val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= 33) extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) + else extras.getParcelable(Intent.EXTRA_KEY_EVENT) as KeyEvent? + Log.d(TAG, "onReceive keyEvent = $keyEvent" ) + + if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) { ClientConfigurator.initialize(context) val serviceIntent = Intent(PLAYBACK_SERVICE_INTENT) serviceIntent.setPackage(context.packageName) - serviceIntent.putExtra(EXTRA_KEYCODE, event.keyCode) - serviceIntent.putExtra(EXTRA_SOURCE, event.source) - serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, event.eventTime > 0 || event.downTime > 0) + serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode) + serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source) + serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0) try { ContextCompat.startForegroundService(context, serviceIntent) } catch (e: Exception) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index a9c7cb17..b72c3f4f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -71,48 +71,7 @@ object AutoDownloads { // likely not needed @UnstableApi open fun autoDownloadEpisodeMedia(context: Context, feeds: List? = null): Runnable? { - return Runnable { -// // true if we should auto download based on network status -//// val networkShouldAutoDl = (isAutoDownloadAllowed) -// val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload) -// // true if we should auto download based on power status -// val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery) -// Logd(TAG, "prepare autoDownloadUndownloadedItems $networkShouldAutoDl $powerShouldAutoDl") -// // we should only auto download if both network AND power are happy -// if (networkShouldAutoDl && powerShouldAutoDl) { -// Logd(TAG, "Performing auto-dl of undownloaded episodes") -// val queueItems = curQueue.episodes -// val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), EpisodeSortOrder.DATE_NEW_OLD) -// Logd(TAG, "newItems: ${newItems.size}") -// val candidates: MutableList = ArrayList(queueItems.size + newItems.size) -// candidates.addAll(queueItems) -// for (newItem in newItems) { -// val feedPrefs = newItem.feed!!.preferences -// if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.autoDownloadFilter!!.shouldAutoDownload(newItem)) candidates.add(newItem) -// } -// // filter items that are not auto downloadable -// val it = candidates.iterator() -// while (it.hasNext()) { -// val item = it.next() -// if (!item.isAutoDownloadEnabled || item.isDownloaded || item.media == null || isCurMedia(item.media) || item.feed?.isLocalFeed == true) -// it.remove() -// } -// val autoDownloadableEpisodes = candidates.size -// val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) -// val deletedEpisodes = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableEpisodes) -// val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED -// val episodeCacheSize = episodeCacheSize -// val episodeSpaceLeft = -// if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) autoDownloadableEpisodes -// else episodeCacheSize - (downloadedEpisodes - deletedEpisodes) -// val itemsToDownload: List = candidates.subList(0, episodeSpaceLeft) -// if (itemsToDownload.isNotEmpty()) { -// Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download") -// for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode) -// } -// } -// else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl") - } + return Runnable {} } /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 4289f2f2..c750c85e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -9,6 +9,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus +import ac.mdiq.podcini.storage.database.Queues.addToQueueSync import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope @@ -284,6 +285,10 @@ object Feeds { if (pubDate == null || priorMostRecentDate == null || priorMostRecentDate.before(pubDate) || priorMostRecentDate == pubDate) { Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate") episode.setNew() + if (savedFeed.preferences?.autoAddNewToQueue == true) { + val q = savedFeed.preferences?.queue + if (q != null) runOnIOScope { addToQueueSync(false, episode, q) } + } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index b98727e0..021cdffd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -133,18 +133,13 @@ object Queues { if (queueModified) { // TODO: handle sorting applySortOrder(qItems, events) -// curQueue.episodes.clear() -// curQueue.episodes.addAll(qItems) curQueue = upsert(curQueue) { it.episodeIds.clear() it.episodeIds.addAll(qItemIds) it.update() } -// curQueue.episodes.addAll(qItems) - for (event in events) EventFlow.postEvent(event) -// EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(updatedItems)) if (markAsUnplayed && markAsUnplayeds.size > 0) setPlayState(Episode.PlayState.UNPLAYED.code, false, *markAsUnplayeds.toTypedArray()) // if (performAutoDownload) autodownloadEpisodeMedia(context) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index f9e1c32e..40aecdce 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor object RealmDB { private val TAG: String = RealmDB::class.simpleName ?: "Anonymous" - private const val SCHEMA_VERSION_NUMBER = 18L + private const val SCHEMA_VERSION_NUMBER = 19L private val ioScope = CoroutineScope(Dispatchers.IO) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt index 5eef2f89..5231c2ee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt @@ -1,5 +1,6 @@ package ac.mdiq.podcini.storage.model +import ac.mdiq.podcini.R import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger @@ -93,6 +94,8 @@ class FeedPreferences : EmbeddedRealmObject { } var queueId: Long = 0L + var autoAddNewToQueue: Boolean = false + @Ignore var autoDownloadFilter: FeedAutoDownloadFilter? = null get() = field ?: FeedAutoDownloadFilter(autoDLInclude, autoDLExclude, autoDLMinDuration, markExcludedPlayed) @@ -121,10 +124,10 @@ class FeedPreferences : EmbeddedRealmObject { } var autoDLPolicyCode: Int = 0 - enum class AutoDLPolicy(val code: Int) { - ONLY_NEW(0), - NEWER(1), - OLDER(2); + enum class AutoDLPolicy(val code: Int, val resId: Int) { + ONLY_NEW(0, R.string.feed_auto_download_new), + NEWER(1, R.string.feed_auto_download_newer), + OLDER(2, R.string.feed_auto_download_older); companion object { fun fromCode(code: Int): AutoDLPolicy { @@ -190,5 +193,6 @@ class FeedPreferences : EmbeddedRealmObject { const val TAG_SEPARATOR: String = "\u001e" val FeedAutoDeleteOptions = AutoDeleteAction.values().map { it.tag } + } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/VolumeAdaptionSetting.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/VolumeAdaptionSetting.kt index 525cbf57..c5ad3d31 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/VolumeAdaptionSetting.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/VolumeAdaptionSetting.kt @@ -1,12 +1,14 @@ package ac.mdiq.podcini.storage.model -enum class VolumeAdaptionSetting(private val value: Int, @JvmField val adaptionFactor: Float) { - OFF(0, 1.0f), - LIGHT_REDUCTION(1, 0.5f), - HEAVY_REDUCTION(2, 0.2f), - LIGHT_BOOST(3, 1.5f), - MEDIUM_BOOST(4, 2f), - HEAVY_BOOST(5, 2.5f); +import ac.mdiq.podcini.R + +enum class VolumeAdaptionSetting(private val value: Int, @JvmField val adaptionFactor: Float, val resId: Int) { + OFF(0, 1.0f, R.string.feed_volume_reduction_off), + LIGHT_REDUCTION(1, 0.5f, R.string.feed_volume_reduction_light), + HEAVY_REDUCTION(2, 0.2f, R.string.feed_volume_reduction_heavy), + LIGHT_BOOST(3, 1.5f, R.string.feed_volume_boost_light), + MEDIUM_BOOST(4, 2f, R.string.feed_volume_boost_medium), + HEAVY_BOOST(5, 2.5f, R.string.feed_volume_boost_heavy); fun toInteger(): Int { return value diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt index 9e5dc88d..41bc3cb5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt @@ -215,11 +215,11 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v } @JvmField - val swipeActions: List = Collections.unmodifiableList( - listOf(NoActionSwipeAction(), AddToQueueSwipeAction(), StartDownloadSwipeAction(), MarkFavoriteSwipeAction(), - TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(), - DeleteSwipeAction(), RemoveFromHistorySwipeAction()) - ) + val swipeActions: List = listOf( + NoActionSwipeAction(), AddToQueueSwipeAction(), + StartDownloadSwipeAction(), MarkFavoriteSwipeAction(), + TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(), + DeleteSwipeAction(), RemoveFromHistorySwipeAction()) private fun getPrefs(tag: String, defaultActions: String): Actions { val prefsString = prefs!!.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index 56152d5b..1b4457eb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -113,13 +113,13 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { override fun onSearchResultClicked(result: SearchPreferenceResult) { when (val screen = result.resourceFile) { - R.xml.feed_settings -> { - val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.feed_settings_label) - builder.setMessage(R.string.pref_feed_settings_dialog_msg) - builder.setPositiveButton(android.R.string.ok, null) - builder.show() - } +// R.xml.feed_settings -> { +// val builder = MaterialAlertDialogBuilder(this) +// builder.setTitle(R.string.feed_settings_label) +// builder.setMessage(R.string.pref_feed_settings_dialog_msg) +// builder.setPositiveButton(android.R.string.ok, null) +// builder.show() +// } R.xml.preferences_notifications -> openScreen(screen) else -> { val fragment = openScreen(result.resourceFile) @@ -197,9 +197,9 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { R.xml.preferences_notifications -> { return R.string.notification_pref_fragment } - R.xml.feed_settings -> { - return R.string.feed_settings_label - } +// R.xml.feed_settings -> { +// return R.string.feed_settings_label +// } R.xml.preferences_swipe -> { return R.string.swipeactions_label } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SkipPreferenceDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SkipPreferenceDialog.kt index 394f6f7e..12163815 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SkipPreferenceDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SkipPreferenceDialog.kt @@ -23,7 +23,6 @@ object SkipPreferenceDialog { val choices = arrayOfNulls(values.size) for (i in values.indices) { if (skipSecs == values[i]) checked = i - choices[i] = String.format(Locale.getDefault(), "%d %s", values[i], context.getString(R.string.time_seconds)) } @@ -31,15 +30,12 @@ object SkipPreferenceDialog { builder.setTitle(if (direction == SkipDirection.SKIP_FORWARD) R.string.pref_fast_forward else R.string.pref_rewind) builder.setSingleChoiceItems(choices, checked) { dialog: DialogInterface, _: Int -> val choice = (dialog as AlertDialog).listView.checkedItemPosition - if (choice < 0 || choice >= values.size) { - System.err.printf("Choice in showSkipPreference is out of bounds %d", choice) - } else { + if (choice < 0 || choice >= values.size) System.err.printf("Choice in showSkipPreference is out of bounds %d", choice) + else { val seconds = values[choice] if (direction == SkipDirection.SKIP_FORWARD) fastForwardSecs = seconds else rewindSecs = seconds - if (textView != null) textView.text = NumberFormat.getInstance().format(seconds.toLong()) - dialog.dismiss() } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt index 5ada2e8c..6b199124 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt @@ -26,7 +26,7 @@ class TagSettingsDialog : DialogFragment() { private var _binding: EditTagsDialogBinding? = null private val binding get() = _binding!! - private var feedList: List = mutableListOf() + private var feedList: MutableList = mutableListOf() private lateinit var displayedTags: MutableList private lateinit var adapter: SimpleChipAdapter @@ -100,9 +100,10 @@ class TagSettingsDialog : DialogFragment() { } @OptIn(UnstableApi::class) private fun updatePreferencesTags(commonTags: Set) { - for (f in feedList) { + for (i in 0..feedList.size-1) { + val f = feedList[i] Logd(TAG, "${f.title} $displayedTags") - upsertBlk(f) { + feedList[i] = upsertBlk(f) { if (it.preferences != null) { it.preferences!!.tags.removeAll(commonTags) it.preferences!!.tags.addAll(displayedTags) @@ -112,7 +113,7 @@ class TagSettingsDialog : DialogFragment() { } private fun setFeedList(feedLst_: List) { - feedList = feedLst_ + feedList = feedLst_.toMutableList() } companion object { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 175c53cd..278e6cbf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -2,21 +2,21 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.AutodownloadFilterDialogBinding -import ac.mdiq.podcini.databinding.FeedPrefSkipDialogBinding import ac.mdiq.podcini.databinding.FeedsettingsBinding import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* +import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDLPolicy import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.Spinner -import ac.mdiq.podcini.ui.dialog.AuthenticationDialog import ac.mdiq.podcini.ui.dialog.TagSettingsDialog import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration import ac.mdiq.podcini.util.Logd @@ -32,11 +32,11 @@ import android.view.ViewGroup import android.widget.CompoundButton import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.OptIn +import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -45,16 +45,16 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.fragment.app.Fragment import androidx.media3.common.util.UnstableApi -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.util.* @@ -67,6 +67,20 @@ class FeedSettingsFragment : Fragment() { private var autoDeletePolicy = "global" private var queues: List? = null + private var notificationPermissionDenied: Boolean = false + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) return@registerForActivityResult + if (notificationPermissionDenied) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", requireContext().packageName, null) + intent.setData(uri) + startActivity(intent) + return@registerForActivityResult + } + Toast.makeText(context, R.string.notification_permission_denied, Toast.LENGTH_LONG).show() + notificationPermissionDenied = true + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FeedsettingsBinding.inflate(inflater) Logd(TAG, "fragment onCreateView") @@ -83,6 +97,7 @@ class FeedSettingsFragment : Fragment() { modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { + // refresh Column { Row(Modifier.fillMaxWidth()) { Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor) @@ -99,8 +114,8 @@ class FeedSettingsFragment : Fragment() { modifier = Modifier.height(24.dp), onCheckedChange = { checked = it - feed = upsertBlk(feed!!) { - it.preferences?.keepUpdated = checked + feed = upsertBlk(feed!!) { f -> + f.preferences?.keepUpdated = checked } } ) @@ -111,6 +126,7 @@ class FeedSettingsFragment : Fragment() { color = textColor ) } + // prefer streaming Column { Row(Modifier.fillMaxWidth()) { Icon(ImageVector.vectorResource(id = R.drawable.ic_stream), "", tint = textColor) @@ -127,8 +143,8 @@ class FeedSettingsFragment : Fragment() { modifier = Modifier.height(24.dp), onCheckedChange = { checked = it - feed = upsertBlk(feed!!) { - it.preferences?.prefStreamOverDownload = checked + feed = upsertBlk(feed!!) { f -> + f.preferences?.prefStreamOverDownload = checked } } ) @@ -139,6 +155,7 @@ class FeedSettingsFragment : Fragment() { color = textColor ) } + // associated queue Column { curPrefQueue = feed?.preferences?.queueTextExt ?: "Default" Row(Modifier.fillMaxWidth()) { @@ -168,6 +185,40 @@ class FeedSettingsFragment : Fragment() { color = textColor ) } + // auto add new to queue + if (curPrefQueue != "None") { + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = androidx.media3.session.R.drawable.media3_icon_queue_add), + "", + tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.audo_add_new_queue), + style = MaterialTheme.typography.h6, + color = textColor + ) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { mutableStateOf(feed?.preferences?.autoAddNewToQueue ?: true) } + Switch( + checked = checked, + modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + feed = upsertBlk(feed!!) { f -> + f.preferences?.autoAddNewToQueue = checked + } + } + ) + } + Text( + text = stringResource(R.string.audo_add_new_queue_summary), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + } + // auto delete Column { Row(Modifier.fillMaxWidth()) { Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) @@ -195,14 +246,304 @@ class FeedSettingsFragment : Fragment() { color = textColor ) } + // tags + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.feed_tags_label), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val dialog = TagSettingsDialog.newInstance(listOf(feed!!)) + dialog.show(parentFragmentManager, TagSettingsDialog.TAG) + }) + ) + } + Text( + text = stringResource(R.string.feed_tags_summary), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // playback speed + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.playback_speed), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + PlaybackSpeedDialog().show() + }) + ) + } + Text( + text = stringResource(R.string.pref_feed_playback_speed_sum), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // auto skip + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.pref_feed_skip), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + AutoSkipDialog(showDialog.value, onDismiss = { showDialog.value = false }) + } + } + } + (view as? ViewGroup)?.addView(composeView) + }) + ) + } + Text( + text = stringResource(R.string.pref_feed_skip_sum), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // volume adaption + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.feed_volume_adapdation), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + VolumeAdaptionDialog(showDialog.value, onDismissRequest = { showDialog.value = false }) + } + } + } + (view as? ViewGroup)?.addView(composeView) + }) + ) + } + Text( + text = stringResource(R.string.feed_volume_adaptation_summary), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // authentication + if (feed?.isLocalFeed != true) { + Column { + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(R.string.authentication_label), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + AuthenticationDialog(showDialog.value, + onDismiss = { showDialog.value = false }) + } + } + } + (view as? ViewGroup)?.addView(composeView) + }) + ) + } + Text( + text = stringResource(R.string.authentication_descr), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + } + if (isEnableAutodownload) { + // auto download + var audoDownloadChecked by remember { mutableStateOf(feed?.preferences?.autoDownload ?: true) } + Column { + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.auto_download_label), + style = MaterialTheme.typography.h6, + color = textColor + ) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = audoDownloadChecked, + modifier = Modifier.height(24.dp), + onCheckedChange = { + audoDownloadChecked = it + feed = upsertBlk(feed!!) { f -> + f.preferences?.autoDownload = audoDownloadChecked + } + } + ) + } + if (!isEnableAutodownload) { + Text( + text = stringResource(R.string.auto_download_disabled_globally), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + } + if (audoDownloadChecked) { + // auto download policy + Column (modifier = Modifier.padding(start = 20.dp)){ + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.feed_auto_download_policy), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + AutoDownloadPolicyDialog(showDialog.value, + onDismissRequest = { showDialog.value = false }) + } + } + } + (view as? ViewGroup)?.addView(composeView) + }) + ) + } + } + // episode cache + Column (modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.pref_episode_cache_title), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + SetEpisodesCacheDialog(showDialog.value, + onDismiss = { showDialog.value = false }) + } + } + } + (view as? ViewGroup)?.addView(composeView) + }) + ) + } + Text( + text = stringResource(R.string.pref_episode_cache_summary), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // counting played + Column (modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.pref_auto_download_counting_played_title), + style = MaterialTheme.typography.h6, + color = textColor + ) + Spacer(modifier = Modifier.weight(1f)) + var checked by remember { + mutableStateOf(feed?.preferences?.countingPlayed ?: true) + } + Switch( + checked = checked, + modifier = Modifier.height(24.dp), + onCheckedChange = { + checked = it + feed = upsertBlk(feed!!) { f -> + f.preferences?.countingPlayed = checked + } + } + ) + } + Text( + text = stringResource(R.string.pref_auto_download_counting_played_summary), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // inclusive filter + Column (modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.episode_inclusive_filters_label), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + object : AutoDownloadFilterPrefDialog(requireContext(), + feed?.preferences!!.autoDownloadFilter!!, + 1) { + @UnstableApi + override fun onConfirmed(filter: FeedAutoDownloadFilter) { + feed = upsertBlk(feed!!) { + it.preferences?.autoDownloadFilter = filter + } + } + }.show() + }) + ) + } + Text( + text = stringResource(R.string.episode_filters_description), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + // exclusive filter + Column (modifier = Modifier.padding(start = 20.dp)) { + Row(Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.episode_exclusive_filters_label), + style = MaterialTheme.typography.h6, + color = textColor, + modifier = Modifier.clickable(onClick = { + object : AutoDownloadFilterPrefDialog(requireContext(), + feed?.preferences!!.autoDownloadFilter!!, + -1) { + @UnstableApi + override fun onConfirmed(filter: FeedAutoDownloadFilter) { + feed = upsertBlk(feed!!) { + it.preferences?.autoDownloadFilter = filter + } + } + }.show() + }) + ) + } + Text( + text = stringResource(R.string.episode_filters_description), + style = MaterialTheme.typography.body2, + color = textColor + ) + } + } + } } } } if (feed != null) { toolbar.subtitle = feed!!.title - parentFragmentManager.beginTransaction() - .replace(R.id.settings_fragment_container, FeedSettingsPreferenceFragment.newInstance(feed!!), "settings_fragment") - .commitAllowingStateLoss() } return binding.root } @@ -254,7 +595,7 @@ class FeedSettingsFragment : Fragment() { verticalAlignment = Alignment.CenterVertically ) { Checkbox(checked = (text == selectedOption), - onCheckedChange = { isChecked -> + onCheckedChange = { Logd(TAG, "row clicked: $text $selectedOption") if (text != selectedOption) { onOptionSelected(text) @@ -287,6 +628,142 @@ class FeedSettingsFragment : Fragment() { } } + @Composable + fun VolumeAdaptionDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { + if (showDialog) { + val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column { + VolumeAdaptionSetting.entries.forEach { item -> + Row(Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = (item == selectedOption), + onCheckedChange = { _ -> + Logd(TAG, "row clicked: $item $selectedOption") + if (item != selectedOption) { + onOptionSelected(item) + feed = upsertBlk(feed!!) { + it.preferences?.volumeAdaptionSetting = item + } + onDismissRequest() + } + } + ) + Text( + text = stringResource(item.resId), + style = MaterialTheme.typography.body1.merge(), +// color = textColor, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } + } + } + } + } + } + + @Composable + fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { + if (showDialog) { + val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDLPolicy.ONLY_NEW) } + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column { + AutoDLPolicy.entries.forEach { item -> + Row(Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = (item == selectedOption), + onCheckedChange = { + Logd(TAG, "row clicked: $item $selectedOption") + if (item != selectedOption) { + onOptionSelected(item) + feed = upsertBlk(feed!!) { + it.preferences?.autoDLPolicy = item + } +// getAutoDeletePolicy() + onDismissRequest() + } + } + ) + Text( + text = stringResource(item.resId), + style = MaterialTheme.typography.body1.merge(), +// color = textColor, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } + } + } + } + } + } + + @Composable + fun SetEpisodesCacheDialog(showDialog: Boolean, onDismiss: () -> Unit) { + if (showDialog) { + Dialog(onDismissRequest = onDismiss) { + Card(modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) } + TextField(value = newCache, + onValueChange = { if (it.toIntOrNull() != null) newCache = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = PositiveIntegerTransform(), + label = { Text("Max episodes allowed") } + ) + Button(onClick = { + if (newCache.isNotEmpty()) { + runOnIOScope { + feed = upsertBlk(feed!!) { + it.preferences?.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 + } + } + onDismiss() + } + }) { + Text("Confirm") + } + } + } + } + } + } + @Composable private fun SetAssociatedQueue(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) { var selected by remember {mutableStateOf(selectedOption)} @@ -347,320 +824,124 @@ class FeedSettingsFragment : Fragment() { } } - class FeedSettingsPreferenceFragment : PreferenceFragmentCompat() { - private var feed: Feed? = null - private var notificationPermissionDenied: Boolean = false - private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> - if (isGranted) return@registerForActivityResult - if (notificationPermissionDenied) { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - val uri = Uri.fromParts("package", requireContext().packageName, null) - intent.setData(uri) - startActivity(intent) - return@registerForActivityResult - } - Toast.makeText(context, R.string.notification_permission_denied, Toast.LENGTH_LONG).show() - notificationPermissionDenied = true - } - override fun onCreateRecyclerView(inflater: LayoutInflater, parent: ViewGroup, state: Bundle?): RecyclerView { - val view = super.onCreateRecyclerView(inflater, parent, state) - // To prevent transition animation because of summary update - view.itemAnimator = null - view.layoutAnimation = null - return view - } - @OptIn(UnstableApi::class) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.feed_settings) - // To prevent displaying partially loaded data - findPreference(Prefs.feedSettingsScreen.name)!!.isVisible = false - if (feed != null) { - if (feed!!.preferences == null) { - feed!!.preferences = FeedPreferences(feed!!.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") - persistFeedPreferences(feed!!) - } - setupAutoDownloadGlobalPreference() - setupAutoDownloadPreference() - setupVolumeAdaptationPreferences() - setupAuthentificationPreference() - updateAutoDownloadPolicy() - setupAutoDownloadPolicy() - setupAutoDownloadCacheSize() - setupCountingPlayedPreference() - setupAutoDownloadFilterPreference() - setupPlaybackSpeedPreference() - setupFeedAutoSkipPreference() - setupTags() - updateVolumeAdaptationValue() - updateAutoDownloadEnabled() - if (feed!!.isLocalFeed) { - findPreference(Prefs.authentication.name)!!.isVisible = false - findPreference(Prefs.autoDownloadCategory.name)!!.isVisible = false - } - findPreference(Prefs.feedSettingsScreen.name)!!.isVisible = true - } - } - override fun onDestroyView() { - Logd(TAG, "onDestroyView") - feed = null - super.onDestroyView() - } - private fun setupFeedAutoSkipPreference() { - findPreference(Prefs.feedAutoSkip.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - object : FeedPreferenceSkipDialog(requireContext(), feed?.preferences!!.introSkip, feed?.preferences!!.endingSkip) { - @UnstableApi - override fun onConfirmed(skipIntro: Int, skipEnding: Int) { - feed = upsertBlk(feed!!) { - it.preferences?.introSkip = skipIntro - it.preferences?.endingSkip = skipEnding + @Composable + fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) { + if (showDialog) { + Dialog(onDismissRequest = onDismiss) { + Card(modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val oldName = feed?.preferences?.username?:"" + var newName by remember { mutableStateOf(oldName) } + TextField(value = newName, + onValueChange = { newName = it }, + label = { Text("Username") } + ) + val oldPW = feed?.preferences?.password?:"" + var newPW by remember { mutableStateOf(oldPW) } + TextField(value = newPW, + onValueChange = { newPW = it }, + label = { Text("Password") } + ) + Button(onClick = { + if (newName.isNotEmpty() && oldName != newName) { + feed = upsertBlk(feed!!) { + it.preferences?.username = newName + it.preferences?.password = newPW + } + Thread({ runOnce(requireContext(), feed) }, "RefreshAfterCredentialChange").start() + onDismiss() } - } - }.show() - - false - } - } - @UnstableApi private fun setupPlaybackSpeedPreference() { - val feedPlaybackSpeedPreference = findPreference(Prefs.feedPlaybackSpeed.name) - feedPlaybackSpeedPreference!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater) - binding.seekBar.setProgressChangedListener { speed: Float? -> - binding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) - } - binding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - binding.seekBar.isEnabled = !isChecked - binding.seekBar.alpha = if (isChecked) 0.4f else 1f - binding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f - } - val speed = feed?.preferences!!.playSpeed - binding.useGlobalCheckbox.isChecked = speed == FeedPreferences.SPEED_USE_GLOBAL - binding.seekBar.updateSpeed(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed) - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.playback_speed) - .setView(binding.root) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - val newSpeed = if (binding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL - else binding.seekBar.currentSpeed - feed = upsertBlk(feed!!) { - it.preferences?.playSpeed = newSpeed + }) { + Text("Confirm") } } - .setNegativeButton(R.string.cancel_label, null) - .show() - true - } - } - @UnstableApi private fun setupAutoDownloadPolicy() { - val policyPref = findPreference(Prefs.feedAutoDownloadPolicy.name) - policyPref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - feed = upsertBlk(feed!!) { - it.preferences?.autoDLPolicyCode = newValue.toString().toInt() } - updateAutoDownloadPolicy() - false - } - } - private fun updateAutoDownloadPolicy() { - val policyPref = findPreference(Prefs.feedAutoDownloadPolicy.name) - when (feed?.preferences!!.autoDLPolicy) { - FeedPreferences.AutoDLPolicy.ONLY_NEW -> policyPref!!.value = FeedPreferences.AutoDLPolicy.ONLY_NEW.ordinal.toString() - FeedPreferences.AutoDLPolicy.NEWER -> policyPref!!.value = FeedPreferences.AutoDLPolicy.NEWER.ordinal.toString() - FeedPreferences.AutoDLPolicy.OLDER -> policyPref!!.value = FeedPreferences.AutoDLPolicy.OLDER.ordinal.toString() - } - } - @UnstableApi private fun setupAutoDownloadCacheSize() { - val cachePref = findPreference(Prefs.feedEpisodeCacheSize.name) - cachePref!!.value = feed?.preferences!!.autoDLMaxEpisodes.toString() - cachePref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - feed = upsertBlk(feed!!) { - it.preferences?.autoDLMaxEpisodes = newValue.toString().toInt() - } - cachePref.value = newValue.toString() - false - } - } - @OptIn(UnstableApi::class) private fun setupCountingPlayedPreference() { - val pref = findPreference(Prefs.countingPlayed.name) - pref!!.isChecked = feed?.preferences!!.countingPlayed - pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - val checked = newValue == true - feed = upsertBlk(feed!!) { - it.preferences?.countingPlayed = checked - } - pref.isChecked = checked - false - } - } - private fun setupAutoDownloadFilterPreference() { - findPreference(Prefs.episodeInclusiveFilter.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - object : AutoDownloadFilterPrefDialog(requireContext(), feed?.preferences!!.autoDownloadFilter!!, 1) { - @UnstableApi - override fun onConfirmed(filter: FeedAutoDownloadFilter) { - feed = upsertBlk(feed!!) { - it.preferences?.autoDownloadFilter = filter - } - } - }.show() - false - } - findPreference(Prefs.episodeExclusiveFilter.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - object : AutoDownloadFilterPrefDialog(requireContext(), feed?.preferences!!.autoDownloadFilter!!, -1) { - @UnstableApi - override fun onConfirmed(filter: FeedAutoDownloadFilter) { - feed = upsertBlk(feed!!) { - it.preferences?.autoDownloadFilter = filter - } - } - }.show() - false - } - } - private fun setupAuthentificationPreference() { - findPreference(Prefs.authentication.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - object : AuthenticationDialog(requireContext(), R.string.authentication_label, true, feed?.preferences!!.username, feed?.preferences!!.password) { - @UnstableApi - override fun onConfirmed(username: String, password: String) { - feed = upsertBlk(feed!!) { - it.preferences?.username = username - it.preferences?.password = password - } - Thread({ runOnce(context, feed) }, "RefreshAfterCredentialChange").start() - } - }.show() - false - } - } - @UnstableApi private fun setupVolumeAdaptationPreferences() { - val volumeAdaptationPreference = findPreference("volumeReduction") ?: return - volumeAdaptationPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> - val vAdapt = when (newValue as String?) { - "off" -> VolumeAdaptionSetting.OFF - "light" -> VolumeAdaptionSetting.LIGHT_REDUCTION - "heavy" -> VolumeAdaptionSetting.HEAVY_REDUCTION - "light_boost" -> VolumeAdaptionSetting.LIGHT_BOOST - "medium_boost" -> VolumeAdaptionSetting.MEDIUM_BOOST - "heavy_boost" -> VolumeAdaptionSetting.HEAVY_BOOST - else -> VolumeAdaptionSetting.OFF - } - feed = upsertBlk(feed!!) { - it.preferences?.volumeAdaptionSetting = vAdapt - } - updateVolumeAdaptationValue() - false - } - } - private fun updateVolumeAdaptationValue() { - val volumeAdaptationPreference = findPreference("volumeReduction") ?: return - when (feed?.preferences?.volumeAdaptionSetting) { - VolumeAdaptionSetting.OFF -> volumeAdaptationPreference.value = "off" - VolumeAdaptionSetting.LIGHT_REDUCTION -> volumeAdaptationPreference.value = "light" - VolumeAdaptionSetting.HEAVY_REDUCTION -> volumeAdaptationPreference.value = "heavy" - VolumeAdaptionSetting.LIGHT_BOOST -> volumeAdaptationPreference.value = "light_boost" - VolumeAdaptionSetting.MEDIUM_BOOST -> volumeAdaptationPreference.value = "medium_boost" - VolumeAdaptionSetting.HEAVY_BOOST -> volumeAdaptationPreference.value = "heavy_boost" - else -> {} - } - } - private fun setupAutoDownloadGlobalPreference() { - if (!isEnableAutodownload || feed?.preferences?.autoDownload != true) { - val autodl = findPreference(Prefs.autoDownload.name) - autodl!!.isChecked = false - autodl.isEnabled = false - autodl.setSummary(R.string.auto_download_disabled_globally) - findPreference(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = false - findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = false - findPreference(Prefs.countingPlayed.name)!!.isEnabled = false - findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = false - findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = false - } - } - @OptIn(UnstableApi::class) private fun setupAutoDownloadPreference() { - val pref = findPreference(Prefs.autoDownload.name) - pref!!.isEnabled = isEnableAutodownload - if (isEnableAutodownload) pref.isChecked = feed?.preferences!!.autoDownload - else { - pref.isChecked = false - pref.setSummary(R.string.auto_download_disabled_globally) - } - pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - val checked = newValue == true - feed = upsertBlk(feed!!) { - it.preferences?.autoDownload = checked - } - updateAutoDownloadEnabled() - pref.isChecked = checked - false - } - } - private fun updateAutoDownloadEnabled() { - if (feed?.preferences != null) { - val enabled = feed!!.preferences!!.autoDownload && isEnableAutodownload - findPreference(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = enabled - findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = enabled - findPreference(Prefs.countingPlayed.name)!!.isEnabled = enabled - findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = enabled - findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = enabled - } - } - private fun setupTags() { - findPreference(Prefs.tags.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - TagSettingsDialog.newInstance(listOf(feed!!)).show(childFragmentManager, TagSettingsDialog.TAG) - true - } - } - - fun setFeed(feed_: Feed) { - feed = feed_ - } - - @Suppress("EnumEntryName") - private enum class Prefs { - feedSettingsScreen, - keepUpdated, - authentication, - associatedQueue, - autoDelete, - feedPlaybackSpeed, - feedAutoSkip, - tags, - autoDownloadCategory, - autoDownload, - feedAutoDownloadPolicy, - feedEpisodeCacheSize, - countingPlayed, - episodeInclusiveFilter, - episodeExclusiveFilter, - } - - companion object { - fun newInstance(feed: Feed): FeedSettingsPreferenceFragment { - val fragment = FeedSettingsPreferenceFragment() - fragment.setFeed(feed) - return fragment } } } - /** - * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. - */ - abstract class FeedPreferenceSkipDialog(context: Context, skipIntroInitialValue: Int, skipEndInitialValue: Int) : MaterialAlertDialogBuilder(context) { - init { - setTitle(R.string.pref_feed_skip) - val binding = FeedPrefSkipDialogBinding.bind(View.inflate(context, R.layout.feed_pref_skip_dialog, null)) - setView(binding.root) - val etxtSkipIntro = binding.etxtSkipIntro - val etxtSkipEnd = binding.etxtSkipEnd - etxtSkipIntro.setText(skipIntroInitialValue.toString()) - etxtSkipEnd.setText(skipEndInitialValue.toString()) - setNegativeButton(R.string.cancel_label, null) - setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> - val skipIntro = try { etxtSkipIntro.text.toString().toInt() } catch (e: NumberFormatException) { 0 } - val skipEnding = try { etxtSkipEnd.text.toString().toInt() } catch (e: NumberFormatException) { 0 } - onConfirmed(skipIntro, skipEnding) + @Composable + fun AutoSkipDialog(showDialog: Boolean, onDismiss: () -> Unit) { + if (showDialog) { + Dialog(onDismissRequest = onDismiss) { + Card(modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) } + TextField(value = intro, + onValueChange = { if (it.toIntOrNull() != null) intro = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = PositiveIntegerTransform(), + label = { Text("Skip first (seconds)") } + ) + var ending by remember { mutableStateOf((feed?.preferences?.endingSkip ?: 0).toString()) } + TextField(value = ending, + onValueChange = { if (it.toIntOrNull() != null) ending = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = PositiveIntegerTransform(), + label = { Text("Skip last (seconds)") } + ) + Button(onClick = { + if (intro.isNotEmpty() || ending.isNotEmpty()) { + feed = upsertBlk(feed!!) { + it.preferences?.introSkip = intro.toIntOrNull() ?: 0 + it.preferences?.endingSkip = ending.toIntOrNull() ?: 0 + } + onDismiss() + } + }) { + Text("Confirm") + } + } + } } } - protected abstract fun onConfirmed(skipIntro: Int, skipEnding: Int) + } + + fun PlaybackSpeedDialog(): AlertDialog { + val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(LayoutInflater.from(requireContext())) + binding.seekBar.setProgressChangedListener { speed: Float? -> + binding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) + } + binding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + binding.seekBar.isEnabled = !isChecked + binding.seekBar.alpha = if (isChecked) 0.4f else 1f + binding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f + } + val speed = feed?.preferences!!.playSpeed + binding.useGlobalCheckbox.isChecked = speed == FeedPreferences.SPEED_USE_GLOBAL + binding.seekBar.updateSpeed(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed) + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.playback_speed) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + val newSpeed = if (binding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL + else binding.seekBar.currentSpeed + feed = upsertBlk(feed!!) { + it.preferences?.playSpeed = newSpeed + } + } + .setNegativeButton(R.string.cancel_label, null) + .create() + } + + class PositiveIntegerTransform : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val trimmedText = text.text.filter { it.isDigit() } + val transformedText = if (trimmedText.isNotEmpty() && trimmedText.toInt() > 0) trimmedText else "" + return TransformedText(AnnotatedString(transformedText), OffsetMapping.Identity) + } } /** @@ -701,8 +982,24 @@ class FeedSettingsFragment : Fragment() { } setupWordsList() setNegativeButton(R.string.cancel_label, null) - setPositiveButton(R.string.confirm_label) { dialog: DialogInterface, which: Int -> - this.onConfirmClick(dialog, which) + setPositiveButton(R.string.confirm_label) { _: DialogInterface, _: Int -> +// this.onConfirmClick(dialog, which) + if (inexcl == -1) { + var minimalDuration = -1 + if (binding.durationCheckBox.isChecked) { + try { + // Store minimal duration in seconds + minimalDuration = binding.episodeFilterDurationText.text.toString().toInt() * 60 + } catch (e: NumberFormatException) { + // Do not change anything on error + } + } + val excludeFilter = toFilterString(termList) + onConfirmed(FeedAutoDownloadFilter(filter.includeFilterRaw, excludeFilter, minimalDuration, binding.markPlayedCheckBox.isChecked)) + } else { + val includeFilter = toFilterString(termList) + onConfirmed(FeedAutoDownloadFilter(includeFilter, filter.excludeFilterRaw, filter.minimalDurationFilter, filter.markExcludedPlayed)) + } } } private fun setupWordsList() { @@ -727,24 +1024,6 @@ class FeedSettingsFragment : Fragment() { } } protected abstract fun onConfirmed(filter: FeedAutoDownloadFilter) - private fun onConfirmClick(dialog: DialogInterface, which: Int) { - if (inexcl == -1) { - var minimalDuration = -1 - if (binding.durationCheckBox.isChecked) { - try { - // Store minimal duration in seconds - minimalDuration = binding.episodeFilterDurationText.text.toString().toInt() * 60 - } catch (e: NumberFormatException) { - // Do not change anything on error - } - } - val excludeFilter = toFilterString(termList) - onConfirmed(FeedAutoDownloadFilter(filter.includeFilterRaw, excludeFilter, minimalDuration, binding.markPlayedCheckBox.isChecked)) - } else { - val includeFilter = toFilterString(termList) - onConfirmed(FeedAutoDownloadFilter(includeFilter, filter.excludeFilterRaw, filter.minimalDurationFilter, filter.markExcludedPlayed)) - } - } private fun toFilterString(words: List?): String { val result = StringBuilder() for (word in words!!) { @@ -756,12 +1035,14 @@ class FeedSettingsFragment : Fragment() { fun setFeed(feed_: Feed) { feed = feed_ + if (feed!!.preferences == null) { + feed!!.preferences = FeedPreferences(feed!!.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") + persistFeedPreferences(feed!!) + } } companion object { private val TAG: String = FeedSettingsFragment::class.simpleName ?: "Anonymous" - private const val EXTRA_FEED_ID = "ac.mdiq.podcini.extra.feedId" - val queueSettingOptions = listOf("Default", "Active", "None", "Custom") fun newInstance(feed: Feed): FeedSettingsFragment { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index f11074d8..6bae5d80 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -70,8 +70,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { private var datasetStats: DatasetStats? = null private lateinit var navAdapter: NavListAdapter -// private var openFolders: MutableSet = HashSet() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) _binding = NavListBinding.inflate(inflater) @@ -92,11 +90,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { insets } -// val preferences: SharedPreferences = requireContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) -// TODO: what is this? -// openFolders = HashSet(prefs!!.getStringSet(PREF_OPEN_FOLDERS, HashSet())!!) // Must not modify - -// loadData() val navList = binding.navRecycler navAdapter = NavListAdapter() navAdapter.setHasStableIds(true) @@ -197,7 +190,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { } } fun getFragmentTags(): List { - return Collections.unmodifiableList(fragmentTags) + return fragmentTags } override fun getItemCount(): Int { return subscriptionOffset @@ -359,7 +352,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { @VisibleForTesting const val PREF_LAST_FRAGMENT_TAG: String = "prefLastFragmentTag" -// private const val PREF_OPEN_FOLDERS = "prefOpenFolders" const val VIEW_TYPE_NAV: Int = 0 const val VIEW_TYPE_SECTION_DIVIDER: Int = 1 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt index c44dfd66..b87bebc2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt @@ -294,7 +294,7 @@ import java.util.* val pos = queueItems.size queueItems.addAll(event.episodes) adapter?.notifyItemRangeInserted(pos, queueItems.size) - adapter?.notifyItemRangeChanged(pos, event.episodes.size); + adapter?.notifyItemRangeChanged(pos, event.episodes.size) } } FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> { @@ -312,7 +312,7 @@ import java.util.* holder?.unbind() queueItems.removeAt(pos) adapter?.notifyItemRemoved(pos) - adapter?.notifyItemRangeChanged(pos, adapter!!.getItemCount()-pos); + adapter?.notifyItemRangeChanged(pos, adapter!!.itemCount -pos) } else { Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}") continue @@ -494,19 +494,16 @@ import java.util.* fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) { if (showDialog) { Dialog(onDismissRequest = onDismiss) { - Card( - modifier = Modifier - .wrapContentSize(align = Alignment.Center) - .padding(16.dp), + Card(modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), shape = RoundedCornerShape(16.dp), ) { - Column( - modifier = Modifier.padding(16.dp), + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { var newName by remember { mutableStateOf(curQueue.name) } - TextField( - value = newName, + TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") } ) @@ -537,19 +534,16 @@ import java.util.* fun AddQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) { if (showDialog) { Dialog(onDismissRequest = onDismiss) { - Card( - modifier = Modifier - .wrapContentSize(align = Alignment.Center) - .padding(16.dp), + Card(modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), shape = RoundedCornerShape(16.dp), ) { - Column( - modifier = Modifier.padding(16.dp), + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { var newName by remember { mutableStateOf("") } - TextField( - value = newName, + TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") } ) 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 95dd68a1..5abacee3 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 @@ -27,6 +27,7 @@ import ac.mdiq.podcini.ui.fragment.FeedSettingsFragment.Companion.queueSettingOp import ac.mdiq.podcini.ui.utils.CoverLoader import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.LiftOnScrollListener +import ac.mdiq.podcini.util.DateFormatter.formatAbbrev import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent @@ -425,8 +426,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec for (f in feedList_) { val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L counterMap[f.id] = d - val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) - f.sortInfo = "Updated: " + dateFormat.format(Date(d)) +// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) + f.sortInfo = "Updated: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) } @@ -434,11 +435,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec val queryString = "feedId == $0 SORT(media.downloadTime DESC)" val counterMap: MutableMap = mutableMapOf() for (f in feedList_) { - val d = - realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L + val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L counterMap[f.id] = d - val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) - f.sortInfo = "Downloaded: " + dateFormat.format(Date(d)) +// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) + f.sortInfo = "Downloaded: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) } @@ -449,8 +449,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec for (f in feedList_) { val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L counterMap[f.id] = d - val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) - f.sortInfo = "Unplayed: " + dateFormat.format(Date(d)) +// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) + f.sortInfo = "Unplayed: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/DateFormatter.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/DateFormatter.kt index d4681de0..cb2af44f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/DateFormatter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/DateFormatter.kt @@ -13,7 +13,7 @@ object DateFormatter { @JvmStatic fun formatRfc822Date(date: Date?): String { val format = SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US) - return format.format(date) + return format.format(date?: Date(0)) } @JvmStatic @@ -25,16 +25,40 @@ object DateFormatter { cal.time = date val withinLastYear = now[Calendar.YEAR] == cal[Calendar.YEAR] var format = DateUtils.FORMAT_ABBREV_ALL - if (withinLastYear) { - format = format or DateUtils.FORMAT_NO_YEAR - } + if (withinLastYear) format = format or DateUtils.FORMAT_NO_YEAR + return DateUtils.formatDateTime(context, date.time, format) } @JvmStatic fun formatForAccessibility(date: Date?): String { if (date == null) return "" - return DateFormat.getDateInstance(DateFormat.LONG).format(date) } + + fun formatDateTimeFlex(date: Date): String { + val now = Date() + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + return when { + isSameDay(date, now) -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) + isSameYear(date, now) -> SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).format(date) + else -> formatter.format(date) + } + } + + fun isSameDay(date1: Date, date2: Date): Boolean { + val cal1 = Calendar.getInstance() + cal1.time = date1 + val cal2 = Calendar.getInstance() + cal2.time = date2 + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) + } + + fun isSameYear(date1: Date, date2: Date): Boolean { + val cal1 = Calendar.getInstance() + cal1.time = date1 + val cal2 = Calendar.getInstance() + cal2.time = date2 + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) + } } diff --git a/app/src/main/res/layout/feedsettings.xml b/app/src/main/res/layout/feedsettings.xml index da75f4ad..b9f26433 100644 --- a/app/src/main/res/layout/feedsettings.xml +++ b/app/src/main/res/layout/feedsettings.xml @@ -23,22 +23,9 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - - + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 30735926..07e1584c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,47 +1,5 @@ - - - @string/feed_auto_download_new - @string/feed_auto_download_newer - @string/feed_auto_download_older - - - 0 - 1 - 2 - - - - @string/global_default - @string/feed_auto_download_always - @string/feed_auto_download_never - - - - global - always - never - - - - @string/feed_volume_reduction_heavy - @string/feed_volume_reduction_light - @string/feed_volume_reduction_off - @string/feed_volume_boost_light - @string/feed_volume_boost_medium - @string/feed_volume_boost_heavy - - - - heavy - light - off - light_boost - medium_boost - heavy_boost - - @string/feed_refresh_never @string/feed_every_hour @@ -92,7 +50,6 @@ 500 @string/pref_episode_cache_unlimited - 5 10 @@ -103,27 +60,6 @@ -1 - - 2 - 3 - 4 - 5 - 10 - 15 - 20 - @string/pref_episode_cache_unlimited - - - 2 - 3 - 4 - 5 - 10 - 15 - 20 - -1 - - @string/pref_mobileUpdate_refresh @string/pref_mobileUpdate_episode_download diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 90bf3595..5fdf634b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -810,6 +810,8 @@ Mark excluded episodes played Keep updated Include this podcast when (auto-)refreshing all podcasts + Auto add new to queue + Automatically add new episodes in this podcast to the designated queue when (auto-)refreshing Auto download is disabled in the main Podcini settings Time played: Total duration (estimate): diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml deleted file mode 100644 index 04b70630..00000000 --- a/app/src/main/res/xml/feed_settings.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/changelog.md b/changelog.md index 9fd23260..0a7dc333 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +# 6.3.5 + +* added more Log.d statements in hope for tracking down the mysterious random playing +* FeedSettings view is all in Jetpack Compose, FeedSettingsPreferenceFragment removed +* in FeedSettings, added "Audo add new to queue" (accissible when associated queue not set to "None") + * when set, new episodes during refresh will be added to the associated queue, regardless of being downloaded +* use adaptive date formats (stripped time) in Subscriptions view + # 6.3.4 * fixed mis-behavior of setting associated queue to Active in FeedSettings