")
writer.append(feed.title)
writer.append(" {
- findPreference(Prefs.project.name)!!.isVisible = false
- val copyrightNotice = Preference(requireContext())
- copyrightNotice.setIcon(R.drawable.ic_info_white)
- copyrightNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
- copyrightNotice.summary = ("This application is based on Podcini."
+ copyrightNoticeText = ("This application is based on Podcini."
+ " The Podcini team does NOT provide support for this unofficial version."
+ " If you can read this message, the developers of this modification"
+ " violate the GNU General Public License (GPL).")
- findPreference(Prefs.project.name)!!.parent!!.addPreference(copyrightNotice)
}
- packageHash == 1297601420 -> {
- val debugNotice = Preference(requireContext())
- debugNotice.setIcon(R.drawable.ic_info_white)
- debugNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
- debugNotice.order = -1
- debugNotice.summary = "This is a development version of Podcini and not meant for daily use"
- findPreference(Prefs.project.name)!!.parent!!.addPreference(debugNotice)
+ packageHash == 1297601420 -> copyrightNoticeText = "This is a development version of Podcini and not meant for daily use"
+ }
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ CustomTheme(requireContext()) {
+ val textColor = MaterialTheme.colorScheme.onSurface
+ val scrollState = rememberScrollState()
+ Column(modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
+ if (copyrightNoticeText.isNotBlank()) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = Icons.Filled.Info, contentDescription = "", tint = Color.Red, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Text(copyrightNoticeText, color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_appearance), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
+ })) {
+ Text(stringResource(R.string.user_interface_label), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.user_interface_sum), color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
+ })) {
+ Text(stringResource(R.string.playback_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.playback_pref_sum), color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(Screens.preferences_downloads)
+ })) {
+ Text(stringResource(R.string.downloads_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.downloads_pref_sum), color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_cloud), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
+ })) {
+ Text(stringResource(R.string.synchronization_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.synchronization_sum), color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_storage), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
+ })) {
+ Text(stringResource(R.string.import_export_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.import_export_summary), color = textColor)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_notifications), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ (activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
+ })) {
+ Text(stringResource(R.string.notification_pref_fragment), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.pref_backup_on_google_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.pref_backup_on_google_sum), color = textColor)
+ }
+ Switch(checked = true, onCheckedChange = {
+ appPrefs.edit().putBoolean(UserPreferences.Prefs.prefOPMLBackup.name, it).apply()
+ // Restart the app
+ val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
+ intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context?.startActivity(intent)
+ })
+ }
+ HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp).padding(top = 10.dp))
+ Text(stringResource(R.string.project_pref), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 15.dp))
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_questionmark), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
+ })) {
+ Text(stringResource(R.string.documentation_support), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chat), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
+ })) {
+ Text(stringResource(R.string.visit_user_forum), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_contribute), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
+ })) {
+ Text(stringResource(R.string.pref_contribute), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_bug), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ startActivity(Intent(activity, BugReportActivity::class.java))
+ })) {
+ Text(stringResource(R.string.bug_report_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
+ Column(modifier = Modifier.weight(1f).clickable(onClick = {
+ parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
+ })) {
+ Text(stringResource(R.string.about_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
}
}
}
- override fun onStart() {
- super.onStart()
- (activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.settings_label)
- }
-
- @SuppressLint("CommitTransaction")
- private fun setupMainScreen() {
- findPreference(Prefs.prefScreenInterface.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
- true
- }
- findPreference(Prefs.prefScreenPlayback.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
- true
- }
- findPreference(Prefs.prefScreenDownloads.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(R.xml.preferences_downloads)
- true
- }
- findPreference(Prefs.prefScreenSynchronization.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
- true
- }
- findPreference(Prefs.prefScreenImportExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
- true
- }
- findPreference(Prefs.notifications.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- (activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
- true
- }
- val switchPreference = findPreference("prefOPMLBackup")
- switchPreference?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- if (newValue is Boolean) {
- // Restart the app
- val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
- intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
- context?.startActivity(intent)
- }
- true
- }
- findPreference(Prefs.prefAbout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
- true
- }
- findPreference(Prefs.prefDocumentation.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
- true
- }
- findPreference(Prefs.prefViewForum.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
- true
- }
- findPreference(Prefs.prefContribute.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
- true
- }
- findPreference(Prefs.prefSendBugReport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- startActivity(Intent(activity, BugReportActivity::class.java))
- true
- }
- }
-
- private fun setupSearch() {
- val searchPreference = findPreference("searchPreference")
- val config = searchPreference!!.searchConfiguration
- config.setActivity((activity as AppCompatActivity))
- config.setFragmentContainerViewId(R.id.settingsContainer)
- config.setBreadcrumbsEnabled(true)
-
- config.index(R.xml.preferences_user_interface).addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
- config.index(R.xml.preferences_playback).addBreadcrumb(getTitleOfPage(R.xml.preferences_playback))
- config.index(R.xml.preferences_downloads).addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
-// config.index(R.xml.preferences_import_export).addBreadcrumb(getTitleOfPage(R.xml.preferences_import_export))
- config.index(R.xml.preferences_autodownload)
- .addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
- .addBreadcrumb(R.string.automation)
- .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.preferences_swipe)
-// .addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
-// .addBreadcrumb(getTitleOfPage(R.xml.preferences_swipe))
- }
-
class AboutFragment : PreferenceFragmentCompat() {
@SuppressLint("CommitTransaction")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
@@ -208,7 +209,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
setContent {
CustomTheme(requireContext()) {
val textColor = MaterialTheme.colorScheme.onSurface
- Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp)) {
Image(painter = painterResource(R.drawable.teaser), contentDescription = "")
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_star), contentDescription = "", tint = textColor)
@@ -324,20 +325,4 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
private class LicenseItem(val title: String, val subtitle: String, val licenseUrl: String, val licenseTextFile: String)
}
}
-
- @Suppress("EnumEntryName")
- private enum class Prefs {
- prefScreenInterface,
- prefScreenPlayback,
- prefScreenDownloads,
- prefScreenImportExport,
- prefScreenSynchronization,
- prefDocumentation,
- prefViewForum,
- prefSendBugReport,
- project,
- prefAbout,
- notifications,
- prefContribute,
- }
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt
index 5173d418..800b3298 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt
@@ -109,9 +109,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
val allButtonNames = context!!.resources.getStringArray(R.array.full_notification_buttons_options)
val buttonIDs = intArrayOf(2, 3, 4)
val exactItems = 2
- val completeListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int ->
- fullNotificationButtons = preferredButtons
- }
+ val completeListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> fullNotificationButtons = preferredButtons }
val title = context.resources.getString(
R.string.pref_full_notification_buttons_title)
@@ -136,9 +134,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
if (!isValid) preferredButtons.removeAt(i)
}
- for (i in checked.indices) {
- if (preferredButtons.contains(buttonIds[i])) checked[i] = true
- }
+ for (i in checked.indices) if (preferredButtons.contains(buttonIds[i])) checked[i] = true
val builder = MaterialAlertDialogBuilder(context!!)
builder.setTitle(title)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
index a5505c48..dd5bd554 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
@@ -18,7 +18,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@@ -41,6 +41,8 @@ import kotlin.math.min
object Episodes {
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
+ private val smartMarkAsPlayedPercent: Int = 95
+
val prefRemoveFromQueueMarkedPlayed by lazy { appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) }
val prefDeleteRemovesFromQueue by lazy { appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false) }
@@ -265,4 +267,32 @@ object Episodes {
e.media = m
return e
}
+
+ @JvmStatic
+ fun indexOfItemWithId(episodes: List, id: Long): Int {
+ for (i in episodes.indices) {
+ val episode = episodes[i]
+ if (episode?.id == id) return i
+ }
+ return -1
+ }
+
+ @JvmStatic
+ fun episodeListContains(episodes: List, itemId: Long): Boolean {
+ return indexOfItemWithId(episodes, itemId) >= 0
+ }
+
+ @JvmStatic
+ fun indexOfItemWithDownloadUrl(items: List, downloadUrl: String): Int {
+ for (i in items.indices) {
+ val item = items[i]
+ if (item?.media?.downloadUrl == downloadUrl) return i
+ }
+ return -1
+ }
+
+ @JvmStatic
+ fun hasAlmostEnded(media: Playable): Boolean {
+ return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
+ }
}
\ No newline at end of file
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 63ac2215..070463fb 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
@@ -5,14 +5,14 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
+import ac.mdiq.podcini.storage.database.Episodes.indexOfItemWithId
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
-import ac.mdiq.podcini.storage.utils.EpisodeUtil.indexOfItemWithId
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@@ -73,9 +73,7 @@ object Queues {
Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find()
val ids = mutableSetOf()
- for (queue in queues) {
- ids.addAll(queue.episodeIds)
- }
+ for (queue in queues) ids.addAll(queue.episodeIds)
return ids
}
@@ -180,9 +178,7 @@ object Queues {
it.episodeIds.clear()
it.update()
}
- for (e in curQueue.episodes) {
- if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
- }
+ for (e in curQueue.episodes) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
curQueue.episodes.clear()
EventFlow.postEvent(FlowEvent.QueueEvent.cleared())
}
@@ -191,9 +187,7 @@ object Queues {
fun removeFromAllQueuesSync(vararg episodes: Episode) {
Logd(TAG, "removeFromAllQueuesSync called ")
val queues = realm.query(PlayQueue::class).find()
- for (q in queues) {
- if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
- }
+ for (q in queues) if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
// ensure curQueue is last updated
if (curQueue.size() > 0) removeFromQueueSync(curQueue, *episodes)
else upsertBlk(curQueue) { it.update() }
@@ -250,9 +244,7 @@ object Queues {
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
- for (e in eList) {
- if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
- }
+ for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
upsert(q) {
it.idsBinList.removeAll(idsInQueuesToRemove)
it.idsBinList.addAll(idsInQueuesToRemove)
@@ -272,9 +264,7 @@ object Queues {
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
- for (e in eList) {
- if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
- }
+ for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
curQueue = upsert(q) {
it.idsBinList.removeAll(idsInQueuesToRemove)
it.idsBinList.addAll(idsInQueuesToRemove)
@@ -313,13 +303,11 @@ object Queues {
}
}
- fun inAnyQueue(episode: Episode): Boolean {
- val queues = realm.query(PlayQueue::class).find()
- for (q in queues) {
- if (q.contains(episode)) return true
- }
- return false
- }
+// fun inAnyQueue(episode: Episode): Boolean {
+// val queues = realm.query(PlayQueue::class).find()
+// for (q in queues) if (q.contains(episode)) return true
+// return false
+// }
class EnqueuePositionPolicy(private val enqueueLocation: EnqueueLocation) {
/**
@@ -349,9 +337,7 @@ object Queues {
}
private fun getPositionOfFirstNonDownloadingItem(startPosition: Int, queueItems: List): Int {
val curQueueSize = queueItems.size
- for (i in startPosition until curQueueSize) {
- if (!isItemAtPositionDownloading(i, queueItems)) return i
- }
+ for (i in startPosition until curQueueSize) if (!isItemAtPositionDownloading(i, queueItems)) return i
return curQueueSize
}
private fun isItemAtPositionDownloading(position: Int, queueItems: List): Boolean {
@@ -362,9 +348,7 @@ object Queues {
private fun getCurrentlyPlayingPosition(queueItems: List, currentPlaying: Playable?): Int {
if (currentPlaying !is EpisodeMedia) return -1
val curPlayingItemId = currentPlaying.episodeOrFetch()?.id
- for (i in queueItems.indices) {
- if (curPlayingItemId == queueItems[i].id) return i
- }
+ for (i in queueItems.indices) if (curPlayingItemId == queueItems[i].id) return i
return -1
}
}
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 50ad7f72..590bbd92 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
@@ -40,7 +40,7 @@ object RealmDB {
SubscriptionLog::class,
Chapter::class))
.name("Podcini.realm")
- .schemaVersion(34)
+ .schemaVersion(35)
.migration({ mContext ->
val oldRealm = mContext.oldRealm // old realm using the previous schema
val newRealm = mContext.newRealm // new realm using the new schema
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 43c84fa8..b5c94e3b 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
@@ -92,6 +92,8 @@ class Episode : RealmObject {
@FullText
var comment: String = ""
+ var commentTime: Long = 0L
+
@Ignore
val isNew: Boolean
get() = playState == PlayState.NEW.code
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt
index bfce83dd..e98f8d9b 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt
@@ -1,6 +1,8 @@
package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
+import java.util.Date
+import java.util.Locale
enum class EpisodeSortOrder(val code: Int, val res: Int) {
DATE_OLD_NEW(1, R.string.publish_date),
@@ -21,6 +23,8 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
DOWNLOAD_DATE_NEW_OLD(16, R.string.download_date),
VIEWS_LOW_HIGH(17, R.string.view_count),
VIEWS_HIGH_LOW(18, R.string.view_count),
+ COMMENT_DATE_OLD_NEW(19, R.string.last_comment_date),
+ COMMENT_DATE_NEW_OLD(20, R.string.last_comment_date),
FEED_TITLE_A_Z(101, R.string.feed_title),
FEED_TITLE_Z_A(102, R.string.feed_title),
@@ -60,5 +64,187 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
return values
}
+
+ /**
+ * Returns a Permutor that sorts a list appropriate to the given sort order.
+ * @return Permutor that sorts a list appropriate to the given sort order.
+ */
+ @JvmStatic
+ fun getPermutor(sortOrder: EpisodeSortOrder): Permutor {
+ var comparator: java.util.Comparator? = null
+ var permutor: Permutor? = null
+
+ when (sortOrder) {
+ EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
+ EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
+ DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
+ DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
+ DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
+ DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
+ EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
+ EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
+ PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
+ PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
+ COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
+ COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
+ DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
+ DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
+ VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
+ VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
+ COMMENT_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f1).compareTo(commentDate(f2)) }
+ COMMENT_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f2).compareTo(playDate(f1)) }
+
+ FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
+ FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
+ RANDOM, RANDOM1 -> permutor = object : Permutor {
+ override fun reorder(queue: MutableList?) {
+ if (!queue.isNullOrEmpty()) queue.shuffle()
+ }
+ }
+ SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor {
+ override fun reorder(queue: MutableList?) {
+ if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, true)
+ }
+ }
+ SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor {
+ override fun reorder(queue: MutableList?) {
+ if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, false)
+ }
+ }
+ SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
+ SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
+ }
+ if (comparator != null) {
+ val comparator2: java.util.Comparator = comparator
+ permutor = object : Permutor {
+ override fun reorder(queue: MutableList?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
+ }
+ }
+ return permutor!!
+ }
+
+ private fun pubDate(item: Episode?): Date {
+ return if (item == null) Date() else Date(item.pubDate)
+ }
+
+ private fun playDate(item: Episode?): Long {
+ return item?.media?.getLastPlayedTime() ?: 0
+ }
+
+ private fun commentDate(item: Episode?): Long {
+ return item?.commentTime ?: 0
+ }
+
+ private fun downloadDate(item: Episode?): Long {
+ return item?.media?.downloadTime ?: 0
+ }
+
+ private fun completeDate(item: Episode?): Date {
+ return item?.media?.playbackCompletionDate ?: Date(0)
+ }
+
+ private fun itemTitle(item: Episode?): String {
+ return (item?.title ?: "").lowercase(Locale.getDefault())
+ }
+
+ private fun duration(item: Episode?): Int {
+ return item?.media?.getDuration() ?: 0
+ }
+
+ private fun size(item: Episode?): Long {
+ return item?.media?.size ?: 0
+ }
+
+ private fun itemLink(item: Episode?): String {
+ return (item?.link ?: "").lowercase(Locale.getDefault())
+ }
+
+ private fun feedTitle(item: Episode?): String {
+ return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
+ }
+
+ private fun viewCount(item: Episode?): Int {
+ return item?.viewCount ?: 0
+ }
+
+ /**
+ * Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
+ * A listener might want to hear episodes from any given feed in pubdate order, but would
+ * prefer a more balanced ordering that avoids having to listen to clusters of consecutive
+ * episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
+ * Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
+ * This method first starts with a queue of the final size, where each slot is empty (null).
+ * It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
+ * The podcast with the second-most number of episodes (`D`) is then
+ * placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
+ * This continues, until we end up with: `EEBEDEECEDEEAEE`.
+ * Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
+ *
+ * @param queue A (modifiable) list of FeedItem elements to be reordered.
+ * @param ascending `true` to use ascending pubdate in the reordering;
+ * `false` for descending.
+ */
+ private fun smartShuffle(queue: MutableList, ascending: Boolean) {
+ // Divide FeedItems into lists by feed
+ val map: MutableMap> = HashMap()
+ for (item in queue) {
+ if (item == null) continue
+ val id = item.feedId
+ if (id != null) {
+ if (!map.containsKey(id)) map[id] = ArrayList()
+ map[id]!!.add(item)
+ }
+ }
+
+ // Sort each individual list by PubDate (ascending/descending)
+ val itemComparator: java.util.Comparator =
+ if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
+ else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
+
+ val feeds: MutableList> = ArrayList()
+ for ((_, value) in map) {
+ value.sortWith(itemComparator)
+ feeds.add(value)
+ }
+
+ val emptySlots = ArrayList()
+ for (i in queue.indices) {
+ queue[i] = null
+ emptySlots.add(i)
+ }
+
+ // Starting with the largest feed, place items spread out through the empty slots in the queue
+ feeds.sortWith { f1: List, f2: List -> f2.size.compareTo(f1.size) }
+ for (feedItems in feeds) {
+ val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
+ val emptySlotIterator = emptySlots.iterator()
+ var skipped = 0
+ var placed = 0
+ while (emptySlotIterator.hasNext()) {
+ val nextEmptySlot = emptySlotIterator.next()
+ skipped++
+ if (skipped >= spread * (placed + 1)) {
+ if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
+ queue[nextEmptySlot] = feedItems[placed]
+ emptySlotIterator.remove()
+ placed++
+ if (placed == feedItems.size) break
+ }
+ }
+ }
+ }
+
+ /**
+ * Interface for passing around list permutor method. This is used for cases where a simple comparator
+ * won't work (e.g. Random, Smart Shuffle, etc)
+ * @param the type of elements in the list
+ */
+ interface Permutor {
+ /**
+ * Reorders the specified list.
+ * @param queue A (modifiable) list of elements to be reordered
+ */
+ fun reorder(queue: MutableList?)
+ }
}
}
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 3f3659d7..dc5a17bb 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
@@ -3,7 +3,7 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -102,6 +102,8 @@ class Feed : RealmObject {
@FullText
var comment: String = ""
+ var commentTime: Long = 0L
+
/**
* Returns the value that uniquely identifies this Feed. If the
* feedIdentifier attribute is not null, it will be returned. Else it will
@@ -288,6 +290,10 @@ class Feed : RealmObject {
paymentLinks.add(funding)
}
+ fun isSynthetic(): Boolean {
+ return id <= MAX_SYNTHETIC_ID
+ }
+
fun getVirtualQueueItems(): List {
var qString = "feedId == $id AND (playState < ${PlayState.SKIPPED.code} OR playState == ${PlayState.AGAIN.code} OR playState == ${PlayState.FOREVER.code})"
// TODO: perhaps need to set prefStreamOverDownload for youtube feeds
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt
deleted file mode 100644
index f475ff47..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package ac.mdiq.podcini.storage.utils
-
-import ac.mdiq.podcini.storage.model.Episode
-import ac.mdiq.podcini.storage.model.Playable
-
-object EpisodeUtil {
- private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
-// val smartMarkAsPlayedSecs: Int
-// get() = appPrefs.getString(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name, "30")!!.toInt()
-
- private val smartMarkAsPlayedPercent: Int = 95
-
- @JvmStatic
- fun indexOfItemWithId(episodes: List, id: Long): Int {
- for (i in episodes.indices) {
- val episode = episodes[i]
- if (episode?.id == id) return i
- }
- return -1
- }
-
- @JvmStatic
- fun episodeListContains(episodes: List, itemId: Long): Boolean {
- return indexOfItemWithId(episodes, itemId) >= 0
- }
-
- @JvmStatic
- fun indexOfItemWithDownloadUrl(items: List, downloadUrl: String): Int {
- for (i in items.indices) {
- val item = items[i]
- if (item?.media?.downloadUrl == downloadUrl) return i
- }
- return -1
- }
-
- @JvmStatic
- fun hasAlmostEnded(media: Playable): Boolean {
- return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt
deleted file mode 100644
index c3227df5..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package ac.mdiq.podcini.storage.utils
-
-import ac.mdiq.podcini.storage.model.Episode
-import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-import java.util.*
-
-/**
- * Provides method for sorting the a list of [Episode] according to rules.
- */
-object EpisodesPermutors {
- /**
- * Returns a Permutor that sorts a list appropriate to the given sort order.
- * @return Permutor that sorts a list appropriate to the given sort order.
- */
- @JvmStatic
- fun getPermutor(sortOrder: EpisodeSortOrder): Permutor {
- var comparator: Comparator? = null
- var permutor: Permutor? = null
-
- when (sortOrder) {
- EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
- EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
- EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
- EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
- EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
- EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
- EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
- EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
- EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
- EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
- EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
- EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
- EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
- EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
- EpisodeSortOrder.VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
- EpisodeSortOrder.VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
-
- EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
- EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
- EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM1 -> permutor = object : Permutor {
- override fun reorder(queue: MutableList?) {
- if (!queue.isNullOrEmpty()) queue.shuffle()
- }
- }
- EpisodeSortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor {
- override fun reorder(queue: MutableList?) {
- if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, true)
- }
- }
- EpisodeSortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor {
- override fun reorder(queue: MutableList?) {
- if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, false)
- }
- }
- EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
- EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
- }
- if (comparator != null) {
- val comparator2: Comparator = comparator
- permutor = object : Permutor {
- override fun reorder(queue: MutableList?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
- }
- }
- return permutor!!
- }
-
- private fun pubDate(item: Episode?): Date {
- return if (item == null) Date() else Date(item.pubDate)
- }
-
- private fun playDate(item: Episode?): Long {
- return item?.media?.getLastPlayedTime() ?: 0
- }
-
- private fun downloadDate(item: Episode?): Long {
- return item?.media?.downloadTime ?: 0
- }
-
- private fun completeDate(item: Episode?): Date {
- return item?.media?.playbackCompletionDate ?: Date(0)
- }
-
- private fun itemTitle(item: Episode?): String {
- return (item?.title ?: "").lowercase(Locale.getDefault())
- }
-
- private fun duration(item: Episode?): Int {
- return item?.media?.getDuration() ?: 0
- }
-
- private fun size(item: Episode?): Long {
- return item?.media?.size ?: 0
- }
-
- private fun itemLink(item: Episode?): String {
- return (item?.link ?: "").lowercase(Locale.getDefault())
- }
-
- private fun feedTitle(item: Episode?): String {
- return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
- }
-
- private fun viewCount(item: Episode?): Int {
- return item?.viewCount ?: 0
- }
-
- /**
- * Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
- * A listener might want to hear episodes from any given feed in pubdate order, but would
- * prefer a more balanced ordering that avoids having to listen to clusters of consecutive
- * episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
- * Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
- * This method first starts with a queue of the final size, where each slot is empty (null).
- * It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
- * The podcast with the second-most number of episodes (`D`) is then
- * placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
- * This continues, until we end up with: `EEBEDEECEDEEAEE`.
- * Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
- *
- * @param queue A (modifiable) list of FeedItem elements to be reordered.
- * @param ascending `true` to use ascending pubdate in the reordering;
- * `false` for descending.
- */
- private fun smartShuffle(queue: MutableList, ascending: Boolean) {
- // Divide FeedItems into lists by feed
- val map: MutableMap> = HashMap()
- for (item in queue) {
- if (item == null) continue
- val id = item.feedId
- if (id != null) {
- if (!map.containsKey(id)) map[id] = ArrayList()
- map[id]!!.add(item)
- }
- }
-
- // Sort each individual list by PubDate (ascending/descending)
- val itemComparator: Comparator =
- if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
- else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
-
- val feeds: MutableList> = ArrayList()
- for ((_, value) in map) {
- value.sortWith(itemComparator)
- feeds.add(value)
- }
-
- val emptySlots = ArrayList()
- for (i in queue.indices) {
- queue[i] = null
- emptySlots.add(i)
- }
-
- // Starting with the largest feed, place items spread out through the empty slots in the queue
- feeds.sortWith { f1: List, f2: List -> f2.size.compareTo(f1.size) }
- for (feedItems in feeds) {
- val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
- val emptySlotIterator = emptySlots.iterator()
- var skipped = 0
- var placed = 0
- while (emptySlotIterator.hasNext()) {
- val nextEmptySlot = emptySlotIterator.next()
- skipped++
- if (skipped >= spread * (placed + 1)) {
- if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
- queue[nextEmptySlot] = feedItems[placed]
- emptySlotIterator.remove()
- placed++
- if (placed == feedItems.size) break
- }
- }
- }
- }
-
- /**
- * Interface for passing around list permutor method. This is used for cases where a simple comparator
- * won't work (e.g. Random, Smart Shuffle, etc)
- * @param the type of elements in the list
- */
- interface Permutor {
- /**
- * Reorders the specified list.
- * @param queue A (modifiable) list of elements to be reordered
- */
- fun reorder(queue: MutableList?)
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt
index 3b53bd1b..bfe0012c 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt
@@ -14,13 +14,15 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.PlayState
-import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
+import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.fragment.*
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
+import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
+import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
@@ -293,19 +295,18 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
setContent {
CustomTheme(fragment.requireContext()) {
if (showEditComment) {
- ChooseRatingDialog(listOf(item)) {
- showEditComment = false
- (fragment.view as? ViewGroup)?.removeView(this@apply)
- }
- var commentTextState by remember { mutableStateOf(TextFieldValue(item.comment)) }
+ val localTime = remember { System.currentTimeMillis() }
+ val initCommentText = remember { (if (item.comment.isBlank()) "" else item.comment + "\n") + fullDateTimeString(localTime) + ":\n" }
+ var commentTextState by remember { mutableStateOf(TextFieldValue(initCommentText)) }
LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it },
onDismissRequest = {
showEditComment = false
(fragment.view as? ViewGroup)?.removeView(this@apply)
},
- onSave = {
- runOnIOScope { upsert(item) { it.comment = commentTextState.text } }
- })
+ onSave = { runOnIOScope { upsert(item) {
+ it.comment = commentTextState.text
+ it.commentTime = localTime
+ } } })
}
}
}
@@ -510,7 +511,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
// delay(ceil((duration * 1.05f).toDouble()).toLong())
// val media: EpisodeMedia? = item.media
// val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
-// if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
+// if (media != null && Episodes.hasAlmostEnded(media) && shouldAutoDelete) {
//// deleteMediaOfEpisode(fragment.requireContext(), item)
// val item_ = deleteMediaSync(fragment.requireContext(), item)
// if (prefDeleteRemovesFromQueue) removeFromQueueSync(null, item_) }
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 43d5c6e6..2a0662b2 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
@@ -54,11 +54,7 @@ class OpmlImportActivity : AppCompatActivity() {
private val titleList: List
get() {
val result: MutableList = ArrayList()
- if (!readElements.isNullOrEmpty()) {
- for (element in readElements!!) {
- if (element.text != null) result.add(element.text!!)
- }
- }
+ if (!readElements.isNullOrEmpty()) for (element in readElements!!) if (element.text != null) result.add(element.text!!)
return result
}
@@ -85,9 +81,7 @@ class OpmlImportActivity : AppCompatActivity() {
binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
val checked = binding.feedlist.checkedItemPositions
var checkedCount = 0
- for (i in 0 until checked.size()) {
- if (checked.valueAt(i)) checkedCount++
- }
+ for (i in 0 until checked.size()) if (checked.valueAt(i)) checkedCount++
if (listAdapter != null) {
if (checkedCount == listAdapter!!.count) {
selectAll.isVisible = false
@@ -104,10 +98,10 @@ class OpmlImportActivity : AppCompatActivity() {
}
binding.butConfirm.setOnClickListener {
binding.progressBar.visibility = View.VISIBLE
+ val checked = binding.feedlist.checkedItemPositions
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
- val checked = binding.feedlist.checkedItemPositions
for (i in 0 until checked.size()) {
if (!checked.valueAt(i)) continue
@@ -144,10 +138,7 @@ class OpmlImportActivity : AppCompatActivity() {
private fun importUri(uri: Uri?) {
if (uri == null) {
- MaterialAlertDialogBuilder(this)
- .setMessage(R.string.opml_import_error_no_file)
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ MaterialAlertDialogBuilder(this).setMessage(R.string.opml_import_error_no_file).setPositiveButton(android.R.string.ok, null).show()
return
}
this.uri = uri
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 9e1dd5f2..0a2ca131 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
@@ -78,7 +78,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
fun openScreen(screen: Int): PreferenceFragmentCompat {
val fragment = when (screen) {
R.xml.preferences_user_interface -> UserInterfacePreferencesFragment()
- R.xml.preferences_downloads -> DownloadsPreferencesFragment()
+// R.xml.preferences_downloads -> DownloadsPreferencesFragment()
// R.xml.preferences_import_export -> ImportExportPreferencesFragment()
R.xml.preferences_autodownload -> AutoDownloadPreferencesFragment()
R.xml.preferences_synchronization -> SynchronizationPreferencesFragment()
@@ -228,7 +228,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
@JvmStatic
fun getTitleOfPage(preferences: Int): Int {
return when (preferences) {
- R.xml.preferences_downloads -> R.string.downloads_pref
+// R.xml.preferences_downloads -> R.string.downloads_pref
R.xml.preferences_autodownload -> R.string.pref_automatic_download_title
R.xml.preferences_playback -> R.string.playback_pref
// R.xml.preferences_import_export -> R.string.import_export_pref
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt
index fbbe8b7e..c909307e 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt
@@ -1,80 +1,24 @@
package ac.mdiq.podcini.ui.compose
import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.geometry.center
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.delay
-import kotlin.math.cos
-import kotlin.math.sin
-
-@Composable
-private fun CustomTextField(
- modifier: Modifier = Modifier,
- leadingIcon: (@Composable () -> Unit)? = null,
- trailingIcon: (@Composable () -> Unit)? = null,
- placeholderText: String = "Placeholder",
- fontSize: TextUnit = MaterialTheme.typography.bodyMedium.fontSize
-) {
- var text by rememberSaveable { mutableStateOf("") }
- BasicTextField(
- modifier = modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small).fillMaxWidth(),
- value = text,
- onValueChange = { text = it },
- singleLine = true,
- cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
- textStyle = LocalTextStyle.current.copy(
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = fontSize
- ),
- decorationBox = { innerTextField ->
- Row(modifier, verticalAlignment = Alignment.CenterVertically) {
- if (leadingIcon != null) leadingIcon()
- Box(Modifier.weight(1f)) {
- if (text.isEmpty())
- Text(text = placeholderText, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), fontSize = fontSize))
- innerTextField()
- }
- if (trailingIcon != null) trailingIcon()
- }
- }
- )
-}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -168,8 +112,6 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
delay(durationMillis)
onDismiss()
}
-
- // Box to display the toast message at the bottom of the screen
Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.BottomCenter) {
Box(modifier = Modifier.background(Color.Black, RoundedCornerShape(8.dp)).padding(8.dp)) {
Text(text = message, color = Color.White, style = MaterialTheme.typography.bodyMedium)
@@ -190,15 +132,11 @@ fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldVa
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
- TextButton(onClick = { onDismissRequest() }) {
- Text("Cancel")
- }
+ TextButton(onClick = { onDismissRequest() }) { Text("Cancel") }
TextButton(onClick = {
onSave(textState.text)
onDismissRequest()
- }) {
- Text("Save")
- }
+ }) { Text("Save") }
}
}
}
@@ -221,9 +159,7 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
Row {
for (columnId in 0 until columns) {
val index = firstIndex + columnId
- Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
- if (index < itemCount) content(index)
- }
+ Box(modifier = Modifier.fillMaxWidth().weight(1f)) { if (index < itemCount) content(index) }
}
}
}
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 1513c3fe..8adcb2b0 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
@@ -34,7 +34,7 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
-import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
+import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
import ac.mdiq.podcini.ui.actions.NullActionButton
@@ -47,6 +47,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
+import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
@@ -395,7 +396,7 @@ fun EraseEpisodesDialog(selected: List, feed: Feed?, onDismissRequest:
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
- if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
+ if (feed == null || !feed.isSynthetic()) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message + ": ${selected.size}")
Text(stringResource(R.string.feed_delete_reason_msg))
@@ -410,8 +411,8 @@ fun EraseEpisodesDialog(selected: List, feed: Feed?, onDismissRequest:
val sLog = SubscriptionLog(e.id, e.title?:"", e.media?.downloadUrl?:"", e.link?:"", SubscriptionLog.Type.Media.name)
upsert(sLog) {
it.rating = e.rating
- it.comment = e.comment
- it.comment += "\nReason to remove:\n" + textState.text
+ it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
+ it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
}
@@ -594,7 +595,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
Text(stringResource(id = R.string.reserve_episodes_label))
}
}
- if (feed != null && feed.id <= MAX_SYNTHETIC_ID) {
+ if (feed != null && feed.isSynthetic()) {
options.add {
Row(modifier = Modifier.padding(horizontal = 16.dp).clickable {
isExpanded = false
@@ -719,8 +720,11 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
modifier = Modifier.width(imageWidth).height(imageHeight)
.clickable(onClick = {
Logd(TAG, "icon clicked!")
- if (selectMode) toggleSelected(vm)
- else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
+ when {
+ selectMode -> toggleSelected(vm)
+ vm.episode.feed != null && vm.episode.feed?.isSynthetic() != true -> activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
+ else -> activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
+ }
}))
Box(Modifier.weight(1f).height(imageHeight)) {
TitleColumn(vm, index, modifier = Modifier.fillMaxWidth())
@@ -735,9 +739,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vm.episode.playState} ${vms[index].episode.title}")
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
vm.actionButton = vm.actionButton.forItem(vm.episode)
- if (vm.actionButton.getLabel() != actionButton.getLabel()) {
- actionButton = vm.actionButton
- }
+ if (vm.actionButton.getLabel() != actionButton.getLabel()) actionButton = vm.actionButton
}
} else {
LaunchedEffect(Unit) {
@@ -1013,18 +1015,16 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
val selectedList = remember { MutableList(item.values.size) { mutableStateOf(false)} }
var expandRow by remember { mutableStateOf(false) }
Row {
- Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
- expandRow = !expandRow
- })
+ Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor,
+ modifier = Modifier.clickable { expandRow = !expandRow })
var lowerSelected by remember { mutableStateOf(false) }
var higherSelected by remember { mutableStateOf(false) }
Spacer(Modifier.weight(1f))
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val hIndex = selectedList.indexOfLast { it.value }
if (hIndex < 0) return@clickable
- if (!lowerSelected) {
- for (i in 0..hIndex) selectedList[i].value = true
- } else {
+ if (!lowerSelected) for (i in 0..hIndex) selectedList[i].value = true
+ else {
for (i in 0..hIndex) selectedList[i].value = false
selectedList[hIndex].value = true
}
@@ -1067,9 +1067,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
if (selectNone) selectedList[index].value = false
LaunchedEffect(Unit) {
- if (filter != null) {
- if (item.values[index].filterId in filter.properties) selectedList[index].value = true
- }
+ if (filter != null && item.values[index].filterId in filter.properties) selectedList[index].value = true
}
OutlinedButton(
modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(),
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt
index 8fc75c5a..43ae0782 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt
@@ -28,6 +28,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter
+import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import android.util.Log
import android.view.Gravity
import androidx.compose.foundation.*
@@ -115,12 +116,12 @@ fun RemoveFeedDialog(feeds: List, onDismissRequest: () -> Unit, callback:
CoroutineScope(Dispatchers.IO).launch {
try {
for (f in feeds) {
- if (f.id > MAX_SYNTHETIC_ID) {
+ if (!f.isSynthetic()) {
val sLog = SubscriptionLog(f.id, f.title ?: "", f.downloadUrl ?: "", f.link ?: "", SubscriptionLog.Type.Feed.name)
upsert(sLog) {
it.rating = f.rating
- it.comment = f.comment
- it.comment += "\nReason to remove:\n" + textState.text
+ it.comment = if (f.comment.isBlank()) "" else (f.comment + "\n")
+ it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
} else {
@@ -128,8 +129,8 @@ fun RemoveFeedDialog(feeds: List, onDismissRequest: () -> Unit, callback:
val sLog = SubscriptionLog(e.id, e.title ?: "", e.media?.downloadUrl ?: "", e.link ?: "", SubscriptionLog.Type.Media.name)
upsert(sLog) {
it.rating = e.rating
- it.comment = e.comment
- it.comment += "\nReason to remove:\n" + textState.text
+ it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
+ it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt
index f4f08dab..f3713e31 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt
@@ -3,16 +3,15 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
+import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
-import ac.mdiq.podcini.ui.fragment.DownloadsFragment.Companion.downloadsSortedOrder
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@@ -28,7 +27,6 @@ import androidx.compose.runtime.setValue
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
-
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -164,7 +162,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
if (loadItemsRunning) return
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
- val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, url)
+ val pos: Int = Episodes.indexOfItemWithDownloadUrl(episodes, url)
if (pos >= 0) {
// episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
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 33dd149b..b55d8d1a 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
@@ -5,6 +5,7 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
+import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
@@ -14,7 +15,6 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.DeleteActionButton
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
@@ -240,7 +240,7 @@ import java.util.*
return // Refreshed anyway
}
// for (downloadUrl in event.urls) {
-// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
+// val pos = Episodes.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
// }
}
@@ -295,7 +295,7 @@ import java.util.*
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i++]
- val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
+ val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
@@ -317,7 +317,7 @@ import java.util.*
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i++]
- val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
+ val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
@@ -356,7 +356,7 @@ import java.util.*
} else {
val mediaUrls: MutableList = ArrayList()
for (url in runningDownloads) {
- if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
+ if (Episodes.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
mediaUrls.add(url)
}
val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList()
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
index 92e64f57..d3434d8b 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
@@ -33,6 +33,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
+import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import android.content.Context
import android.os.Bundle
import android.speech.tts.TextToSpeech
@@ -154,11 +155,17 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
fun MainView() {
val textColor = MaterialTheme.colorScheme.onSurface
+
var showEditComment by remember { mutableStateOf(false) }
+ val localTime = remember { System.currentTimeMillis() }
+ var editCommentText by remember { mutableStateOf(TextFieldValue((if (episode?.comment.isNullOrBlank()) "" else episode!!.comment + "\n") + fullDateTimeString(localTime) + ":\n")) }
var commentTextState by remember { mutableStateOf(TextFieldValue(episode?.comment?:"")) }
- if (showEditComment) LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it }, onDismissRequest = {showEditComment = false},
+ if (showEditComment) LargeTextEditingDialog(textState = editCommentText, onTextChange = { editCommentText = it }, onDismissRequest = {showEditComment = false},
onSave = {
- runOnIOScope { if (episode != null) episode = upsert(episode!!) { it.comment = commentTextState.text } }
+ runOnIOScope { if (episode != null) episode = upsert(episode!!) {
+ it.comment = editCommentText.text
+ it.commentTime = localTime
+ } }
})
var showChooseRatingDialog by remember { mutableStateOf(false) }
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
index a5d5e350..22337d42 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
@@ -16,7 +16,7 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Rating
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt
index 8f6ce6bf..1ae6701e 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt
@@ -13,7 +13,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Feed
-import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.FeedFunding
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.ui.activity.MainActivity
@@ -24,6 +23,7 @@ import ac.mdiq.podcini.ui.compose.RemoveFeedDialog
import ac.mdiq.podcini.ui.fragment.StatisticsFragment.Companion.FeedStatisticsDialog
import ac.mdiq.podcini.ui.utils.TransitionEffect
import ac.mdiq.podcini.util.*
+import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import android.R.string
import android.app.Activity
import android.content.*
@@ -214,12 +214,17 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun DetailUI() {
val scrollState = rememberScrollState()
var showEditComment by remember { mutableStateOf(false) }
+ val localTime = remember { System.currentTimeMillis() }
+ var editCommentText by remember { mutableStateOf(TextFieldValue((if (feed.comment.isBlank()) "" else feed.comment + "\n") + fullDateTimeString(localTime) + ":\n")) }
var commentTextState by remember { mutableStateOf(TextFieldValue(feed.comment)) }
- if (showEditComment) LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it }, onDismissRequest = {showEditComment = false},
+ if (showEditComment) LargeTextEditingDialog(textState = editCommentText, onTextChange = { editCommentText = it }, onDismissRequest = {showEditComment = false},
onSave = {
runOnIOScope {
- feed = upsert(feed) { it.comment = commentTextState.text }
- rating = feed.rating
+ feed = upsert(feed) {
+ it.comment = editCommentText.text
+ it.commentTime = localTime
+ }
+ rating = feed.rating
}
})
var showFeedStats by remember { mutableStateOf(false) }
@@ -236,7 +241,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
modifier = Modifier.padding(start = 15.dp, top = 10.dp, bottom = 5.dp).clickable { showEditComment = true })
Text(commentTextState.text, color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 15.dp, bottom = 10.dp))
- if (feed.id > MAX_SYNTHETIC_ID) {
+ if (!feed.isSynthetic()) {
Text(stringResource(R.string.url_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp))
Text(text = txtvUrl ?: "", color = textColor, modifier = Modifier.clickable {
if (feed.downloadUrl != null) {
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 ccf5ee38..50c102ac 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
@@ -7,7 +7,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
import ac.mdiq.podcini.util.EventFlow
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 ee874be9..1aa6ce0d 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
@@ -12,6 +12,7 @@ import ac.mdiq.podcini.playback.service.PlaybackService.Companion.mediaBrowser
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
+import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
import ac.mdiq.podcini.storage.database.Queues.moveInQueueSync
@@ -21,9 +22,8 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.storage.utils.DurationConverter
-import ac.mdiq.podcini.storage.utils.EpisodeUtil
-import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
@@ -356,7 +356,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
if (event.episodes.isNotEmpty()) {
for (e in event.episodes) {
- val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
+ val pos: Int = Episodes.indexOfItemWithId(queueItems, e.id)
if (pos >= 0) {
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
// queueItems[pos].stopMonitoring.value = true
@@ -390,7 +390,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
- val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
+ val pos: Int = Episodes.indexOfItemWithId(queueItems, event.episode.id)
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
if (pos >= 0) vms[pos].isPlayingState = event.isPlaying()
}
@@ -400,7 +400,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (loadItemsRunning) return
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
- val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url)
+ val pos: Int = Episodes.indexOfItemWithDownloadUrl(queueItems.toList(), url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
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 d2ba8110..6f64e705 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
@@ -4,12 +4,12 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.net.feed.searcher.CombinedSearcher
+import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
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.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
@@ -230,7 +230,7 @@ class SearchFragment : Fragment() {
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
for (url in event.urls) {
- val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, url)
+ val pos: Int = Episodes.indexOfItemWithDownloadUrl(results, url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
}
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 f7c88ecc..6e4af6af 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
@@ -579,29 +579,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
selectMode = false
Logd(TAG, "ic_playback_speed: ${selected.size}")
showSpeedDialog = true
-
-// val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater)
-// vBinding.seekBar.setProgressChangedListener { speed: Float? ->
-// vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
-// }
-// vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
-// vBinding.seekBar.isEnabled = !isChecked
-// vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f
-// vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f
-// }
-// vBinding.seekBar.updateSpeed(1.0f)
-// MaterialAlertDialogBuilder(activity)
-// .setTitle(R.string.playback_speed)
-// .setView(vBinding.root)
-// .setPositiveButton("OK") { _: DialogInterface?, _: Int ->
-// val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
-// else vBinding.seekBar.currentSpeed
-// saveFeedPreferences { it: FeedPreferences ->
-// it.playSpeed = newSpeed
-// }
-// }
-// .setNegativeButton(R.string.cancel_label, null)
-// .show()
}) {
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "")
Text(stringResource(id = R.string.playback_speed)) } },
@@ -1454,10 +1431,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
class PreferenceSwitchDialog(private var context: Context, private val title: String, private val text: String) {
private var onPreferenceChangedListener: OnPreferenceChangedListener? = null
interface OnPreferenceChangedListener {
- /**
- * Notified when user confirms preference
- * @param enabled The preference
- */
fun preferenceChanged(enabled: Boolean)
}
fun openDialog() {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt
index c94abc91..a03a3647 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt
@@ -4,11 +4,12 @@ import android.content.Context
import android.text.format.DateUtils
import java.text.DateFormat
import java.text.SimpleDateFormat
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
import java.util.*
-/**
- * Formats dates.
- */
object MiscFormatter {
@JvmStatic
fun formatRfc822Date(date: Date?): String {
@@ -16,6 +17,19 @@ object MiscFormatter {
return format.format(date?: Date(0))
}
+ fun localDateTimeString(): String {
+ val currentTime = LocalDateTime.now()
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+ return currentTime.format(formatter)
+ }
+
+ fun fullDateTimeString(time: Long): String {
+ val instant = Instant.ofEpochMilli(time)
+ val localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+ return localDateTime.format(formatter)
+ }
+
@JvmStatic
fun formatAbbrev(context: Context?, date: Date?): String {
if (date == null) return ""
diff --git a/app/src/main/res/layout/choose_data_folder_dialog.xml b/app/src/main/res/layout/choose_data_folder_dialog.xml
deleted file mode 100644
index 15f372c2..00000000
--- a/app/src/main/res/layout/choose_data_folder_dialog.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/choose_data_folder_dialog_entry.xml b/app/src/main/res/layout/choose_data_folder_dialog_entry.xml
deleted file mode 100644
index fe1c003c..00000000
--- a/app/src/main/res/layout/choose_data_folder_dialog_entry.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 6c3d9f8d..e67fbbff 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -352,7 +352,7 @@
تنزيلات
الفترة الزمنية للتحديثات، بيانات الجوال، التنزيل التلقائي، الحذف التلقائي
حدث البودكاستات
- حدد الفترة الزمنية التي سيقوم Podcini فيها تلقائيا بالبحث عن حلقات جديدة
+
أبداً
كل ساعة
كل 2 ساعتين
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 47ae82ce..598f61cc 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -328,7 +328,7 @@
Baixades
Intèrval d\'actualització, dades mòbils, baixades automàtiques, esborrat automàtic
Actualitza els pòdcasts
- Especifiqueu un interval segons el qual Podcini cercarà nous episodis automàticament
+
Mai
Cada hora
Cada 2 hores
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index cd285c40..63a9f391 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -357,7 +357,7 @@
Stahování
Interval aktualizace, Mobilní data, Automatické stahování, Automatické mazání
Aktualizovat podcasty
- Zadejte interval, ve kterém bude Podcini automaticky vyhledávat nové epizody
+
Nikdy
Každou hodinu
Každé 2 hodiny
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index f96fac9b..6251e759 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -342,7 +342,7 @@
Overførsler
Opdateringsinterval, Mobildata, Automatisk overførsel, Automatisk sletning
Opdater podcasts
- Angiv et interval, hvormed Podcini automatisk søger efter nye afsnit.
+
Aldrig
Hver time
Hver 2. time
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 43a6d999..a624b75f 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -346,7 +346,7 @@
Downloads
Aktualisierungsintervall, Mobile Daten, Automatischer Download, Automatisches Löschen
Podcasts aktualisieren
- Ein Intervall festlegen, in dem Podcini automatisch nach neuen Episoden sucht
+
Niemals
Jede Stunde
Alle 2 Stunden
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index cc36c3a6..59512322 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -348,7 +348,7 @@
Descargas
Intervalo de actualización, datos móviles, descargas automáticas, borrado automático
Actualizar pódcasts
- Especifique un intervalo para buscar nuevos episodios automáticamente
+
Nunca
Cada hora
Cada 2 horas
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 2344e44b..b61a6ac1 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -329,7 +329,7 @@
بارگیریها
دورهٔ بارگیری، دادهٔ همراه، بارگیری خودکار، حذف خودکار
پادکست ها را بازخوانی کنید
- مشخّص کردن دورهای برای گشتن خودکار آنتناپاد به دنبال قسمتهای جدید
+
هرگز
هر ساعت
هر ۲ ساعت
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 2b5d7472..fae77d6f 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -319,7 +319,7 @@
Kuulokkeiden ohjaimet, ohitusaikavälit, jono
Lataukset
Päivitä podcastit
- Määritä aikaväli tai tietty aika etsiä uusia jaksoja automaattisesti
+
Ei koskaan
Tunnin välein
2 tunnin välein
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 826374f2..c9907082 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -350,7 +350,7 @@
Téléchargements
Fréquence de mise à jour, utilisation de la connexion mobile, téléchargements et suppressions automatiques
Actualiser les podcasts
- Choisir la fréquence de mise à jour automatique des épisodes par Podcini
+
Jamais
Toutes les heures
Toutes les 2 heures
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 1ef2a02e..c2c3d303 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -341,7 +341,7 @@
Descargas
Intervalo de actualización, Datos móbiles, Descarga automática, Eliminación automática
Actualizar podcast
- Indicar cada canto tempo debe Podcini comprobar se hai episodios novos
+
Nunca
Cada hora
Cada 2 horas
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 4f9d2c6a..1a68d403 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -266,7 +266,7 @@
Kontrol headphone, Jangka waktu lewati, Antrean
Waktu pembaharuan, Jaringan seluler, Unduh otomatis, Hapus otomatis
Segarkan podcast
- Tentukan jangka waktu di mana Podcini secara otomatis mencari episode baru
+
Jangan pernah
Setiap jam
Tiap 2 jam
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index cd861c67..6473e2ce 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -350,7 +350,7 @@
Download
Intervallo di aggiornamento, rete mobile, download ed eliminazione automatici
Aggiorna i podcast
- Definisce l\'intervallo di ricerca automatica dei nuovi episodi.
+
Mai
Ogni ora
Ogni 2 ore
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 51bbd190..659245f7 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -353,7 +353,7 @@
הורדות
הפרש בין עדכונים, חיבור סלולרי, הורדה אוטומטית, מחיקה אוטומטית
רענון פודקאסטים
- נא לציין הפרש או מועד מסוים לחיפוש פרקים אוטומטית על ידי אנטנה־פּוֹד
+
אף פעם לא
כל שעה
כל שעתיים
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 10cfaa2f..e0889bde 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -317,7 +317,7 @@
다운로드
업데이트 주기, 휴대전화 망 데이터, 자동 다운로드, 자동 삭제
팟캐스트 새로고침
- 새로운 에피소드가 있는지 자동으로 확인할 주기를 지정합니다
+
안 함
매 시간
매 2시간
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 0f19f1c1..0bc35247 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -321,7 +321,7 @@
Nedlastinger
Oppdaterings-intervall, Mobil data, Automatisk nedlasting, Automatisk sletting
Oppdater podkaster
- Spesifiser en intervall eller et spesifikt tidspunkt når det skal sjekkes automatisk for nye episoder
+
Aldri
Hver time
Hver andre time
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 9f130680..47790dff 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -330,7 +330,7 @@
Downloads
Intervalo de atualização, Dados móveis, Download automático, Exclusão automática
Atualizar podcasts
- Especifique um intervalo em que o Podcini procura novos episódios automaticamente
+
Nunca
A cada hora
A cada 2 horas
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 8db3a89a..86d7fc7e 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -348,7 +348,7 @@
Descargas
Intervalo de atualização, dados móveis, descargas e eliminação automática
Recarregar podcasts
- Defina um intervalo para que Podcini procure episódios automaticamente
+
Nunca
A cada 1 hora
A cada 2 horas
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index ed0a0ac3..a89a9543 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -347,7 +347,7 @@
Descărcări
Interval actualizare, Date mobile, Descărcări automate, Ștergeri automate
Reîmprospătează podcasturile
- Specifică un anumit interval în care Podcini caută episoade noi automat
+
Nicio data
La fiecare oră
La fiecare 2 ore
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 71ddd9ea..e4fd113e 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -333,7 +333,7 @@
Загрузки
Интервал обновления, мобильная сеть, автоматизация
Обновить подкасты
- Укажите интервал с которым Podcini будет автоматически искать обновления выпусков
+
Никогда
Каждый час
Каждые 2 часа
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index ad8e0c56..3d614dd2 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -352,7 +352,7 @@
Sťahovanie
Interval aktualizácie, Mobilné dáta, Automatické sťahovanie, Automatické mazanie
Aktualizovať podcasty
- Zvoliť interval alebo čas v ktorom Podcini automaticky hľadá nové epizódy
+
Nikdy
Každú hodinu
Každé 2 hodiny
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 8b68b82e..918b2ff3 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -341,7 +341,7 @@
Nedladdningar
Uppdateringsintervall, Mobildata, Automatisk nedladdning, Automatisk radering
Uppdatera podcasts
- Ange ett intervall med vilket Podcini letar efter nya episoder automatiskt
+
Aldrig
Varje timme
Varje 2 timmar
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 2b457e18..af776e49 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -328,7 +328,7 @@
İndirilenler
Güncelleme aralığı, Mobil veri, Otomatik indirme, Otomatik silme
Podcastleri güncelle
- Podcini\'un yeni bölümleri otomatik olarak arayacağı zaman aralığını belirtin
+
Hiçbir zaman
Saatte bir
2 saatte bir
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f1997092..e252fc5e 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -353,7 +353,7 @@
Завантаження
Інтервал оновлення, Мобільні дані, Автоматичне завантаження, Автоматичне видалення
Оновити подкасти
- Вкажіть інтервал, з яким Podcini автоматично шукатиме нові епізоди
+
Ніколи
Щогодини
Кожні 2 години
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index ea7e533b..b80ea450 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -341,7 +341,7 @@
下载
更新间隔、移动数据、自动下载、自动删除
刷新播客
- 指定 Podcini 自动查找新节目的间隔
+
永不
每 1 小时
每 2 小时
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 7f60e8d9..8511f856 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,25 +1,25 @@
-
- - @string/feed_refresh_never
- - @string/feed_every_hour
- - @string/feed_every_2_hours
- - @string/feed_every_4_hours
- - @string/feed_every_8_hours
- - @string/feed_every_12_hours
- - @string/feed_every_24_hours
- - @string/feed_every_72_hours
-
-
- - 0
- - 1
- - 2
- - 4
- - 8
- - 12
- - 24
- - 72
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -59,26 +59,25 @@
- -1
-
- - @string/pref_mobileUpdate_refresh
- - @string/pref_mobileUpdate_episode_download
- - @string/pref_mobileUpdate_auto_download
- - @string/pref_mobileUpdate_streaming
- - @string/pref_mobileUpdate_images
- - @string/synchronization_pref
-
-
- - feed_refresh
- - episode_download
- - auto_download
- - streaming
- - images
- - sync
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- images
- - sync
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c52c41ea..56b5e863 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -388,6 +388,7 @@
View count
Date
Count
+ Comment date
Played date
Completed date
Duration
@@ -478,7 +479,7 @@
Downloads
Update interval, Mobile data, Automatic download, Automatic deletion
Refresh podcasts
- Specify an interval at which Podcini looks for new episodes automatically
+ Specify an interval (in hours, 0 means never) at which Podcini looks for new episodes automatically.
Never
Every hour
Every 2 hours
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
deleted file mode 100644
index aea0b12f..00000000
--- a/app/src/main/res/xml/preferences.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/preferences_downloads.xml b/app/src/main/res/xml/preferences_downloads.xml
deleted file mode 100644
index 57cb9bd6..00000000
--- a/app/src/main/res/xml/preferences_downloads.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/changelog.md b/changelog.md
index b20d4f4d..79753f16 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,14 @@
+# 6.14.7
+
+* corrected some deeplinks in manifest file on OPMLActivity
+* added commentTime in Episode and Feed to record the time of comment/opinion added
+* when adding/editing a comment/opinion, a time stamp is automatically added in the text field
+* when removing a feed or an episode, a time stamp is automatically added in the text field
+* a new sorting item on episodes based on commentTime
+* in episodes list, if an episode belongs to a synthetic feed, tapping on the image will bring up EpisodeInfo instead of FeedInfo
+* some class restructuring
+* more preferences fragments are in Compose
+
# 6.14.6
* fixed issue of unable to input in rename feed