diff --git a/app/build.gradle b/app/build.gradle
index 03dfff21..810e4156 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
- versionCode 3020307
- versionName "6.14.8"
+ versionCode 3020308
+ versionName "6.15.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 51a74434..b7d00313 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -190,32 +190,6 @@
android:resource="@xml/player_widget_info"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -257,6 +231,7 @@
+
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt
index 5354be0c..855342e3 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt
@@ -233,16 +233,16 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
{ ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() },
applicationContext.getString(R.string.download_error_details)))
}
- private fun getDownloadLogsIntent(context: Context): PendingIntent {
- val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent()
- return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- }
- private fun getDownloadsIntent(context: Context): PendingIntent {
- val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent()
- return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- }
+// private fun getDownloadLogsIntent(context: Context): PendingIntent {
+// val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent()
+// return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
+// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+// }
+// private fun getDownloadsIntent(context: Context): PendingIntent {
+// val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent()
+// return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
+// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+// }
private fun sendErrorNotification(title: String) {
// TODO: need to get number of subscribers in SharedFlow
// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) {
@@ -254,7 +254,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
.setContentTitle(applicationContext.getString(R.string.download_report_title))
.setContentText(applicationContext.getString(R.string.download_error_tap_for_details))
.setSmallIcon(R.drawable.ic_notification_sync_error)
- .setContentIntent(getDownloadLogsIntent(applicationContext))
+// .setContentIntent(getDownloadLogsIntent(applicationContext))
.setAutoCancel(true)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -277,7 +277,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
.setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes))
.setContentText(contentText)
.setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
- .setContentIntent(getDownloadsIntent(applicationContext))
+// .setContentIntent(getDownloadsIntent(applicationContext))
.setAutoCancel(false)
.setOngoing(true)
.setWhen(0)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt
index ad178d86..9ece23a0 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt
@@ -245,7 +245,7 @@ class WifiSyncService(val context: Context, params: WorkerParameters) : SyncSer
// only push downloaded items
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD)
- val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
+ val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.superb.name), EpisodeSortOrder.DATE_NEW_OLD)
val comItems = mutableSetOf()
comItems.addAll(pausedItems)
comItems.addAll(readItems)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt
index 14eca8be..34b16aa2 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt
@@ -16,6 +16,10 @@ import android.util.Log
import android.util.Xml
import androidx.core.app.ActivityCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.apache.commons.io.input.BOMInputStream
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -164,54 +168,51 @@ class OpmlTransporter {
}
companion object {
- fun startImport(context: Context, uri: Uri) {
+ fun startImport(context: Context, uri: Uri, CB: (List)->Unit) {
val TAG = "OpmlTransporter"
-// CoroutineScope(Dispatchers.IO).launch {
- try {
- val opmlFileStream = context.contentResolver.openInputStream(uri)
- val bomInputStream = BOMInputStream(opmlFileStream)
- val bom = bomInputStream.bom
- val charsetName = if (bom == null) "UTF-8" else bom.charsetName
- val reader: Reader = InputStreamReader(bomInputStream, charsetName)
- val opmlReader = OpmlReader()
- val result = opmlReader.readDocument(reader)
- reader.close()
-// withContext(Dispatchers.Main) {
-// binding.progressBar.visibility = View.GONE
- Logd(TAG, "Parsing was successful")
-// readElements = result
-// }
- } catch (e: Throwable) {
-// withContext(Dispatchers.Main) {
- Logd(TAG, Log.getStackTraceString(e))
- val message = if (e.message == null) "" else e.message!!
- if (message.lowercase().contains("permission")) {
- val permission = ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
- if (permission != PackageManager.PERMISSION_GRANTED) {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ val opmlFileStream = context.contentResolver.openInputStream(uri)
+ val bomInputStream = BOMInputStream(opmlFileStream)
+ val bom = bomInputStream.bom
+ val charsetName = if (bom == null) "UTF-8" else bom.charsetName
+ val reader: Reader = InputStreamReader(bomInputStream, charsetName)
+ val opmlReader = OpmlReader()
+ val result = opmlReader.readDocument(reader)
+ reader.close()
+ withContext(Dispatchers.Main) { CB(result) }
+ } catch (e: Throwable) {
+ withContext(Dispatchers.Main) {
+ Logd(TAG, Log.getStackTraceString(e))
+ val message = if (e.message == null) "" else e.message!!
+ if (message.lowercase().contains("permission")) {
+ val permission = ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
+ if (permission != PackageManager.PERMISSION_GRANTED) {
// requestPermission()
- return
- }
- }
-// binding.progressBar.visibility = View.GONE
- val alert = MaterialAlertDialogBuilder(context)
- alert.setTitle(R.string.error_label)
- val userReadable = context.getString(R.string.opml_reader_error)
- val details = e.message
- val total = """
+ CB(listOf())
+ return@withContext
+ }
+ }
+ val alert = MaterialAlertDialogBuilder(context)
+ alert.setTitle(R.string.error_label)
+ val userReadable = context.getString(R.string.opml_reader_error)
+ val details = e.message
+ val total = """
$userReadable
$details
""".trimIndent()
- val errorMessage = SpannableString(total)
- errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- alert.setMessage(errorMessage)
- alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+ val errorMessage = SpannableString(total)
+ errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ alert.setMessage(errorMessage)
+ alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// finish()
+ }
+ alert.show()
+ CB(listOf())
+ }
}
- alert.show()
-// }
}
-// }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
index 42ea8f9b..7c0ca087 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
@@ -50,10 +50,12 @@ object UserPreferences {
val isThemeColorTinted: Boolean
get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false)
+ // not using this
var hiddenDrawerItems: List
get() {
- val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "")
- return hiddenItems?.split(",") ?: listOf()
+ return listOf()
+// val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "")
+// return hiddenItems?.split(",") ?: listOf()
}
set(items) {
val str = items.joinToString()
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
index 0e8c5e3b..f6268887 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
@@ -22,6 +22,7 @@ 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.Feed.Companion.MAX_NATURAL_SYNTHETIC_ID
+import ac.mdiq.podcini.storage.model.FeedPreferences.AudioType
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
@@ -481,6 +482,13 @@ object Feeds {
return !feed.isLocalFeed || isAutoDeleteLocal
}
+ fun createYTSyndicates() {
+ getYoutubeSyndicate(true, false)
+ getYoutubeSyndicate(false, false)
+ getYoutubeSyndicate(true, true)
+ getYoutubeSyndicate(false, true)
+ }
+
private fun getYoutubeSyndicate(video: Boolean, music: Boolean): Feed {
var feedId: Long = if (video) 1 else 2
if (music) feedId += 2 // music feed takes ids 3 and 4
@@ -492,6 +500,7 @@ object Feeds {
feed = createSynthetic(feedId, name)
feed.type = Feed.FeedType.YOUTUBE.name
feed.hasVideoMedia = video
+ feed.preferences!!.audioTypeSetting = if (music) AudioType.MOVIE else AudioType.SPEECH
feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
upsertBlk(feed) {}
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
@@ -509,8 +518,26 @@ object Feeds {
episode.feedId = feed.id
episode.media?.id = episode.id
upsertBlk(episode) {}
- feed.episodes.add(episode)
- upsertBlk(feed) {}
+ upsertBlk(feed) {
+ it.episodes.add(episode)
+ }
+ EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
+ return 1
+ }
+
+ fun addToSyndicate(episode: Episode, feed: Feed) : Int {
+ Logd(TAG, "addToYoutubeSyndicate: feed: ${feed.title}")
+ if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return 2
+
+ Logd(TAG, "addToSyndicate adding new episode: ${episode.title}")
+ episode.feed = feed
+ episode.id = Feed.newId()
+ episode.feedId = feed.id
+ episode.media?.id = episode.id
+ upsertBlk(episode) {}
+ upsertBlk(feed) {
+ it.episodes.add(episode)
+ }
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
return 1
}
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 eba8b805..0ea8f095 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
@@ -51,7 +51,7 @@ object Queues {
*/
var queueKeepSortedOrder: EpisodeSortOrder?
get() {
- val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default")
+ val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default")!!
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
set(sortOrder) {
@@ -61,8 +61,8 @@ object Queues {
var enqueueLocation: EnqueueLocation
get() {
- val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name)
- try { return EnqueueLocation.valueOf(valStr!!)
+ val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name)!!
+ try { return EnqueueLocation.valueOf(valStr)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
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 590bbd92..23c35965 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(35)
+ .schemaVersion(36)
.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 b5c94e3b..1292ac22 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
@@ -56,6 +56,11 @@ class Episode : RealmObject {
var feedId: Long? = null
+ // parent in these refers to the original parent of the content (shared)
+ var parentTitle: String? = null
+
+ var parentURL: String? = null
+
var podcastIndexChapterUrl: String? = null
var playState: Int
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt
index a5348ddd..ca8708ff 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt
@@ -7,13 +7,17 @@ import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable {
val properties: HashSet = setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()}.toHashSet()
- val showQueued: Boolean = properties.contains(States.queued.name)
- val showNotQueued: Boolean = properties.contains(States.not_queued.name)
+// val showQueued: Boolean = properties.contains(States.queued.name)
+// val showNotQueued: Boolean = properties.contains(States.not_queued.name)
val showDownloaded: Boolean = properties.contains(States.downloaded.name)
val showNotDownloaded: Boolean = properties.contains(States.not_downloaded.name)
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
+ fun add(vararg properties_: String) {
+ properties.addAll(setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()})
+ }
+
fun queryString(): String {
val statements: MutableList = mutableListOf()
val mediaTypeQuerys = mutableListOf()
@@ -37,7 +41,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.bad.name)) ratingQuerys.add(" rating == ${Rating.BAD.code} ")
if (properties.contains(States.neutral.name)) ratingQuerys.add(" rating == ${Rating.OK.code} ")
if (properties.contains(States.good.name)) ratingQuerys.add(" rating == ${Rating.GOOD.code} ")
- if (properties.contains(States.favorite.name)) ratingQuerys.add(" rating == ${Rating.SUPER.code} ")
+ if (properties.contains(States.superb.name)) ratingQuerys.add(" rating == ${Rating.SUPER.code} ")
if (ratingQuerys.isNotEmpty()) {
val query = StringBuilder(" (" + ratingQuerys[0])
if (ratingQuerys.size > 1) for (r in ratingQuerys.subList(1, ratingQuerys.size)) {
@@ -135,8 +139,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
no_media,
has_comments,
no_comments,
- queued,
- not_queued,
+// queued,
+// not_queued,
downloaded,
not_downloaded,
auto_downloadable,
@@ -146,7 +150,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
bad,
neutral,
good,
- favorite,
+ superb,
}
enum class EpisodesFilterGroup(val nameRes: Int, vararg values_: ItemProperties) {
@@ -155,7 +159,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
ItemProperties(R.string.bad, States.bad.name),
ItemProperties(R.string.OK, States.neutral.name),
ItemProperties(R.string.good, States.good.name),
- ItemProperties(R.string.Super, States.favorite.name),
+ ItemProperties(R.string.Super, States.superb.name),
),
PLAY_STATE(R.string.playstate, ItemProperties(R.string.unspecified, States.unspecified.name),
ItemProperties(R.string.building, States.building.name),
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 e98f8d9b..2780266c 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
@@ -38,25 +38,26 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
* Converts the string representation to its enum value. If the string value is unknown,
* the given default value is returned.
*/
- fun parseWithDefault(value: String?, defaultValue: EpisodeSortOrder): EpisodeSortOrder {
- return try { valueOf(value!!) } catch (e: IllegalArgumentException) { defaultValue }
+ fun parseWithDefault(value: String, defaultValue: EpisodeSortOrder): EpisodeSortOrder {
+ return try { valueOf(value) } catch (e: IllegalArgumentException) { defaultValue }
}
- fun fromCodeString(codeStr: String?): EpisodeSortOrder? {
- if (codeStr.isNullOrEmpty()) return null
+ fun fromCodeString(codeStr: String?): EpisodeSortOrder {
+ if (codeStr.isNullOrEmpty()) return EPISODE_TITLE_A_Z
val code = codeStr.toInt()
for (sortOrder in entries) {
if (sortOrder.code == code) return sortOrder
}
- throw IllegalArgumentException("Unsupported code: $code")
+ return EPISODE_TITLE_A_Z
+// throw IllegalArgumentException("Unsupported code: $code")
}
- fun fromCode(code: Int): EpisodeSortOrder? {
- return enumValues().firstOrNull { it.code == code }
+ fun fromCode(code: Int): EpisodeSortOrder {
+ return enumValues().firstOrNull { it.code == code } ?: EPISODE_TITLE_A_Z
}
- fun toCodeString(sortOrder: EpisodeSortOrder?): String? {
- return sortOrder?.code?.toString()
+ fun toCodeString(sortOrder: EpisodeSortOrder): String? {
+ return sortOrder.code.toString()
}
fun valuesOf(stringValues: Array): Array {
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 bfe0012c..58f802a9 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
@@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.actions
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
-import ac.mdiq.podcini.storage.database.Episodes.setPlayState
+import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
@@ -14,7 +14,6 @@ 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.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.fragment.*
@@ -22,7 +21,6 @@ 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
@@ -55,7 +53,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
-import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import java.util.*
@@ -70,30 +67,14 @@ interface SwipeAction {
@DrawableRes
fun getActionColor(): Int
- fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter)
-
- fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
- return false
- }
+ fun performAction(item: Episode, fragment: Fragment)
}
class SwipeActions(private val fragment: Fragment, private val tag: String) : DefaultLifecycleObserver {
-
- @set:JvmName("setFilterProperty")
- var filter: EpisodeFilter? = null
- var actions: Actions
-
- init {
- actions = getPrefs(tag)
- }
+ var actions: Actions = getPrefs(tag, "")
override fun onStart(owner: LifecycleOwner) {
- actions = getPrefs(tag)
- }
-
- @JvmName("setFilterFunction")
- fun setFilter(filter: EpisodeFilter?) {
- this.filter = filter
+ actions = getPrefs(tag, "")
}
fun showDialog() {
@@ -106,7 +87,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
showDialog.value = false
(fragment.view as? ViewGroup)?.removeView(this@apply)
}) {
- actions = getPrefs(this@SwipeActions.tag)
+ actions = getPrefs(this@SwipeActions.tag, "")
// TODO: remove the need of event
EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent())
}
@@ -162,7 +143,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.add_to_queue_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
addToQueue(item)
}
}
@@ -180,7 +161,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.combo_action)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
var showDialog by remember { mutableStateOf(true) }
@@ -195,7 +176,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
for (action in swipeActions) {
if (action.getId() == ActionTypes.NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
- action.performAction(item, fragment, filter)
+ action.performAction(item, fragment)
showDialog = false
(fragment.view as? ViewGroup)?.removeView(this@apply)
}) {
@@ -231,20 +212,18 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.delete_episode_label)
}
- override fun performAction(item_: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item_: Episode, fragment: Fragment) {
var item = item_
if (!item.isDownloaded && item.feed?.isLocalFeed != true) return
val media = item.media
if (media != null) {
val almostEnded = hasAlmostEnded(media)
- if (almostEnded && item.playState < PlayState.PLAYED.code) item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, resetMediaPosition = true, removeFromQueue = false) }
+ if (almostEnded && item.playState < PlayState.PLAYED.code)
+ item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, resetMediaPosition = true, removeFromQueue = false) }
if (almostEnded) item = upsertBlk(item) { it.media?.playbackCompletionDate = Date() }
}
deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item))
}
- override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
- return filter.showDownloaded && (item.isDownloaded || item.feed?.isLocalFeed == true)
- }
}
class SetRatingSwipeAction : SwipeAction {
@@ -260,7 +239,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.set_rating_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showChooseRatingDialog by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -289,7 +268,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.add_opinion_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showEditComment by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -328,7 +307,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.no_action_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {}
+ override fun performAction(item: Episode, fragment: Fragment) {}
}
class RemoveFromHistorySwipeAction : SwipeAction {
@@ -346,7 +325,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.remove_history_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
val playbackCompletionDate: Date? = item.media?.playbackCompletionDate
val lastPlayedDate = item.media?.lastPlayedTime
setHistoryDates(item)
@@ -356,9 +335,6 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
.setAction(fragment.getString(R.string.undo)) {
if (playbackCompletionDate != null) setHistoryDates(item, lastPlayedDate?:0, playbackCompletionDate) }
}
- override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
- return true
- }
private fun setHistoryDates(episode: Episode, lastPlayed: Long = 0, completed: Date = Date(0)) {
runOnIOScope {
val episode_ = realm.query(Episode::class).query("id == $0", episode.id).first().find()
@@ -386,8 +362,8 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.remove_from_queue_label)
}
- override fun performAction(item_: Episode, fragment: Fragment, filter: EpisodeFilter) {
- val position: Int = curQueue.episodes.indexOf(item_)
+ override fun performAction(item_: Episode, fragment: Fragment) {
+// val position: Int = curQueue.episodes.indexOf(item_)
var item = item_
val media = item.media
if (media != null) {
@@ -398,36 +374,6 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
if (item.playState < PlayState.SKIPPED.code) item = runBlocking { setPlayStateSync(PlayState.SKIPPED.code, item, resetMediaPosition = false, removeFromQueue = false) }
// removeFromQueue(item)
runOnIOScope { removeFromQueueSync(curQueue, item) }
- if (willRemove(filter, item)) {
- (fragment.requireActivity() as MainActivity).showSnackbarAbovePlayer(fragment.resources.getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG)
- .setAction(fragment.getString(R.string.undo)) {
- addToQueueAt(item, position)
- }
- }
- }
- override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
- return filter.showQueued || filter.showNotQueued
- }
- /**
- * Inserts a Episode in the queue at the specified index. The 'read'-attribute of the Episode will be set to
- * true. If the Episode is already in the queue, the queue will not be modified.
- * @param episode the Episode that should be added to the queue.
- * @param index Destination index. Must be in range 0..queue.size()
- * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size()
- */
-
- private fun addToQueueAt(episode: Episode, index: Int) : Job {
- return runOnIOScope {
- if (curQueue.episodeIds.contains(episode.id)) return@runOnIOScope
- if (episode.isNew) setPlayState(PlayState.UNPLAYED.code, false, episode)
- curQueue = upsert(curQueue) {
- it.episodeIds.add(index, episode.id)
- it.update()
- }
-// curQueue.episodes.add(index, episode)
- EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, index))
-// if (performAutoDownload) autodownloadEpisodeMedia(context)
- }
}
}
@@ -444,7 +390,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.put_in_queue_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showPutToQueueDialog by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -473,7 +419,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.download_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
if (!item.isDownloaded && item.feed != null && !item.feed!!.isLocalFeed) {
DownloadActionButton(item).onClick(fragment.requireContext())
}
@@ -493,7 +439,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.set_play_state_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showPlayStateDialog by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -531,7 +477,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.shelve_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showShelveDialog by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -560,7 +506,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
override fun getTitle(context: Context): String {
return context.getString(R.string.erase_episodes_label)
}
- override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
+ override fun performAction(item: Episode, fragment: Fragment) {
var showEraseDialog by mutableStateOf(true)
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
@@ -600,16 +546,6 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
return Actions(prefsString)
}
- fun getPrefs(tag: String): Actions {
- return getPrefs(tag, "")
- }
-
- @JvmStatic
- fun getPrefsWithDefaults(tag: String): Actions {
- val defaultActions = "${ActionTypes.NO_ACTION.name},${ActionTypes.NO_ACTION.name}"
- return getPrefs(tag, defaultActions)
- }
-
// fun isSwipeActionEnabled(tag: String): Boolean {
// return prefs!!.getBoolean(KEY_PREFIX_NO_ACTION + tag, true)
// }
@@ -638,7 +574,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onSurface
- val actions = getPrefsWithDefaults(tag)
+ val actions = getPrefs(tag, "${ActionTypes.NO_ACTION.name},${ActionTypes.NO_ACTION.name}")
val leftAction = remember { mutableStateOf(actions.left) }
val rightAction = remember { mutableStateOf(actions.right) }
var keys = swipeActions
@@ -679,15 +615,18 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
Dialog(onDismissRequest = { onDismissRequest() }) {
var forFragment = ""
when (tag) {
- AllEpisodesFragment.TAG -> {
+// AllEpisodesFragment.TAG -> {
+// forFragment = stringResource(R.string.episodes_label)
+// keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name) }
+// }
+ EpisodesFragment.TAG -> {
forFragment = stringResource(R.string.episodes_label)
- keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name) }
- }
- DownloadsFragment.TAG -> {
- forFragment = stringResource(R.string.downloads_label)
- keys = keys.filter { a: SwipeAction ->
- (!a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name) && !a.getId().equals(ActionTypes.START_DOWNLOAD.name)) }
}
+// DownloadsFragment.TAG -> {
+// forFragment = stringResource(R.string.downloads_label)
+// keys = keys.filter { a: SwipeAction ->
+// (!a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name) && !a.getId().equals(ActionTypes.START_DOWNLOAD.name)) }
+// }
FeedEpisodesFragment.TAG -> {
forFragment = stringResource(R.string.subscription)
keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name) }
@@ -698,10 +637,10 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
(!a.getId().equals(ActionTypes.ADD_TO_QUEUE.name) && !a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name)) }.toList()
// keys = keys.filter { a: SwipeAction -> (!a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name)) }
}
- HistoryFragment.TAG -> {
- forFragment = stringResource(R.string.playback_history_label)
- keys = keys.toList()
- }
+// HistoryFragment.TAG -> {
+// forFragment = stringResource(R.string.playback_history_label)
+// keys = keys.toList()
+// }
else -> {}
}
if (tag != QueuesFragment.TAG) keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_QUEUE.name) }
@@ -740,9 +679,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent())
callback()
onDismissRequest()
- }) {
- Text("Confirm")
- }
+ }) { Text("Confirm") }
}
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
index a636e7a8..b3840d54 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
@@ -456,11 +456,11 @@ class MainActivity : CastEnabledActivity() {
val fragment: Fragment
when (tag) {
QueuesFragment.TAG -> fragment = QueuesFragment()
- AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
- DownloadsFragment.TAG -> fragment = DownloadsFragment()
+ EpisodesFragment.TAG -> fragment = EpisodesFragment()
+// AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
+// DownloadsFragment.TAG -> fragment = DownloadsFragment()
LogsFragment.TAG -> fragment = LogsFragment()
-// SubscriptionLogFragment.TAG -> fragment = SubscriptionLogFragment()
- HistoryFragment.TAG -> fragment = HistoryFragment()
+// HistoryFragment.TAG -> fragment = HistoryFragment()
OnlineSearchFragment.TAG -> fragment = OnlineSearchFragment()
SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment()
StatisticsFragment.TAG -> fragment = StatisticsFragment()
@@ -734,9 +734,9 @@ class MainActivity : CastEnabledActivity() {
"/deeplink/main" -> {
val feature = uri.getQueryParameter("page") ?: return
when (feature) {
- "DOWNLOADS" -> loadFragment(DownloadsFragment.TAG, null)
- "HISTORY" -> loadFragment(HistoryFragment.TAG, null)
- "EPISODES" -> loadFragment(AllEpisodesFragment.TAG, null)
+// "DOWNLOADS" -> loadFragment(DownloadsFragment.TAG, null)
+// "HISTORY" -> loadFragment(HistoryFragment.TAG, null)
+ "EPISODES" -> loadFragment(EpisodesFragment.TAG, null)
"QUEUE" -> loadFragment(QueuesFragment.TAG, null)
"SUBSCRIPTIONS" -> loadFragment(SubscriptionsFragment.TAG, null)
"STATISTCS" -> loadFragment(StatisticsFragment.TAG, null)
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
deleted file mode 100644
index 2a0662b2..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt
+++ /dev/null
@@ -1,248 +0,0 @@
-package ac.mdiq.podcini.ui.activity
-
-import ac.mdiq.podcini.R
-import ac.mdiq.podcini.databinding.OpmlSelectionBinding
-import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
-import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
-import ac.mdiq.podcini.storage.database.Feeds.updateFeed
-import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlElement
-import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
-import ac.mdiq.podcini.storage.model.Feed
-import ac.mdiq.podcini.util.Logd
-import android.Manifest
-import android.content.DialogInterface
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Bundle
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.style.ForegroundColorSpan
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.AdapterView
-import android.widget.AdapterView.OnItemClickListener
-import android.widget.ArrayAdapter
-import android.widget.ListView
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.lifecycle.lifecycleScope
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.apache.commons.io.input.BOMInputStream
-import java.io.InputStreamReader
-import java.io.Reader
-
-class OpmlImportActivity : AppCompatActivity() {
- private var uri: Uri? = null
- private var _binding: OpmlSelectionBinding? = null
- private val binding get() = _binding!!
-
- private lateinit var selectAll: MenuItem
- private lateinit var deselectAll: MenuItem
-
- private var listAdapter: ArrayAdapter? = null
- private var readElements: ArrayList? = null
-
- private val titleList: List
- get() {
- val result: MutableList = ArrayList()
- if (!readElements.isNullOrEmpty()) for (element in readElements!!) if (element.text != null) result.add(element.text!!)
- return result
- }
-
- private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
- if (isGranted) startImport()
- else {
- MaterialAlertDialogBuilder(this)
- .setMessage(R.string.opml_import_ask_read_permission)
- .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
- .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
- .show()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- setTheme(getTheme(this))
- super.onCreate(savedInstanceState)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- _binding = OpmlSelectionBinding.inflate(layoutInflater)
- setContentView(binding.root)
- Logd(TAG, "onCreate")
-
- binding.feedlist.choiceMode = ListView.CHOICE_MODE_MULTIPLE
- binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
- val checked = binding.feedlist.checkedItemPositions
- var checkedCount = 0
- for (i in 0 until checked.size()) if (checked.valueAt(i)) checkedCount++
- if (listAdapter != null) {
- if (checkedCount == listAdapter!!.count) {
- selectAll.isVisible = false
- deselectAll.isVisible = true
- } else {
- deselectAll.isVisible = false
- selectAll.isVisible = true
- }
- }
- }
- binding.butCancel.setOnClickListener {
- setResult(RESULT_CANCELED)
- finish()
- }
- binding.butConfirm.setOnClickListener {
- binding.progressBar.visibility = View.VISIBLE
- val checked = binding.feedlist.checkedItemPositions
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- for (i in 0 until checked.size()) {
- if (!checked.valueAt(i)) continue
-
- if (!readElements.isNullOrEmpty()) {
- val element = readElements!![checked.keyAt(i)]
- val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast")
- feed.episodes.clear()
- updateFeed(this@OpmlImportActivity, feed, false)
- }
- }
- runOnce(this@OpmlImportActivity)
- }
- binding.progressBar.visibility = View.GONE
- val intent = Intent(this@OpmlImportActivity, MainActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- finish()
- } catch (e: Throwable) {
- e.printStackTrace()
- binding.progressBar.visibility = View.GONE
- Toast.makeText(this@OpmlImportActivity, (e.message ?: "Import error"), Toast.LENGTH_LONG).show()
- }
- }
- }
-
- var uri = intent.data
- if (uri != null && uri.toString().startsWith("/")) uri = Uri.parse("file://$uri")
- else {
- val extraText = intent.getStringExtra(Intent.EXTRA_TEXT)
- if (extraText != null) uri = Uri.parse(extraText)
- }
- importUri(uri)
- }
-
- 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()
- return
- }
- this.uri = uri
- startImport()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- super.onCreateOptionsMenu(menu)
- val inflater = menuInflater
- inflater.inflate(R.menu.opml_selection_options, menu)
- selectAll = menu.findItem(R.id.select_all_item)
- deselectAll = menu.findItem(R.id.deselect_all_item)
- deselectAll.isVisible = false
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- val itemId = item.itemId
- when (itemId) {
- R.id.select_all_item -> {
- selectAll.isVisible = false
- selectAllItems(true)
- deselectAll.isVisible = true
- return true
- }
- R.id.deselect_all_item -> {
- deselectAll.isVisible = false
- selectAllItems(false)
- selectAll.isVisible = true
- return true
- }
- android.R.id.home -> finish()
- }
- return false
- }
-
- private fun selectAllItems(b: Boolean) {
- for (i in 0 until binding.feedlist.count) {
- binding.feedlist.setItemChecked(i, b)
- }
- }
-
- private fun requestPermission() {
- requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
- }
-
- /** Starts the import process. */
- private fun startImport() {
- binding.progressBar.visibility = View.VISIBLE
-
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- val opmlFileStream = contentResolver.openInputStream(uri!!)
- val bomInputStream = BOMInputStream(opmlFileStream)
- val bom = bomInputStream.bom
- val charsetName = if (bom == null) "UTF-8" else bom.charsetName
- val reader: Reader = InputStreamReader(bomInputStream, charsetName)
- val opmlReader = OpmlReader()
- val result = opmlReader.readDocument(reader)
- reader.close()
- withContext(Dispatchers.Main) {
- binding.progressBar.visibility = View.GONE
- Logd(TAG, "Parsing was successful")
- readElements = result
- listAdapter = ArrayAdapter(this@OpmlImportActivity, android.R.layout.simple_list_item_multiple_choice, titleList)
- binding.feedlist.adapter = listAdapter
- }
- } catch (e: Throwable) {
- withContext(Dispatchers.Main) {
- Logd(TAG, Log.getStackTraceString(e))
- val message = if (e.message == null) "" else e.message!!
- if (message.lowercase().contains("permission")) {
- val permission = ActivityCompat.checkSelfPermission(this@OpmlImportActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
- if (permission != PackageManager.PERMISSION_GRANTED) {
- requestPermission()
- return@withContext
- }
- }
- binding.progressBar.visibility = View.GONE
- val alert = MaterialAlertDialogBuilder(this@OpmlImportActivity)
- alert.setTitle(R.string.error_label)
- val userReadable = getString(R.string.opml_reader_error)
- val details = e.message
- val total = """
- $userReadable
-
- $details
- """.trimIndent()
- val errorMessage = SpannableString(total)
- errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- alert.setMessage(errorMessage)
- alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() }
- alert.show()
- }
- }
- }
- }
-
- override fun onDestroy() {
- _binding = null
- super.onDestroy()
- }
-
- companion object {
- private val TAG: String = OpmlImportActivity::class.simpleName ?: "Anonymous"
- }
-}
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 d4e6fd13..3d664af1 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
@@ -9,6 +9,7 @@ import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
+import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.sync.SyncService
import ac.mdiq.podcini.net.sync.SyncService.Companion.isValidGuid
import ac.mdiq.podcini.net.sync.SynchronizationCredentials
@@ -28,14 +29,13 @@ import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.prefPlaybackSpeed
import ac.mdiq.podcini.preferences.*
+import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlElement
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
-import ac.mdiq.podcini.preferences.UserPreferences.defaultPage
import ac.mdiq.podcini.preferences.UserPreferences.fallbackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.fullNotificationButtons
-import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode
@@ -45,6 +45,7 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
+import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
@@ -52,9 +53,9 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.SwipeActionsDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
+import ac.mdiq.podcini.ui.compose.OpmlImportSelectionDialog
import ac.mdiq.podcini.ui.compose.PlaybackSpeedDialog
import ac.mdiq.podcini.ui.fragment.*
-import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.navMap
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@@ -77,6 +78,7 @@ import android.text.method.HideReturnsTransformationMethod
import android.text.method.PasswordTransformationMethod
import android.util.Log
import android.util.Patterns
+import android.util.SparseBooleanArray
import android.view.*
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
@@ -120,8 +122,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
-import com.bytehamster.lib.preferencesearch.SearchPreferenceResult
-import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
@@ -146,7 +146,6 @@ import java.util.*
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import javax.xml.parsers.DocumentBuilderFactory
-import kotlin.Throws
import kotlin.math.round
/**
@@ -594,12 +593,12 @@ class PreferenceActivity : AppCompatActivity() {
ActivityCompat.recreate(requireActivity())
})
}
- Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
- drawerPreferencesDialog(requireContext(), null)
- })) {
- Text(stringResource(R.string.pref_nav_drawer_items_title), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
- Text(stringResource(R.string.pref_nav_drawer_items_sum), color = textColor)
- }
+// Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
+// drawerPreferencesDialog(requireContext(), null)
+// })) {
+// Text(stringResource(R.string.pref_nav_drawer_items_title), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+// Text(stringResource(R.string.pref_nav_drawer_items_sum), color = textColor)
+// }
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.pref_episode_cover_title), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
@@ -731,44 +730,43 @@ class PreferenceActivity : AppCompatActivity() {
enum class DefaultPages(val res: Int) {
SubscriptionsFragment(R.string.subscriptions_label),
QueuesFragment(R.string.queue_label),
- AllEpisodesFragment(R.string.episodes_label),
- DownloadsFragment(R.string.downloads_label),
+ EpisodesFragment(R.string.episodes_label),
+// DownloadsFragment(R.string.downloads_label),
PlaybackHistoryFragment(R.string.playback_history_label),
AddFeedFragment(R.string.add_feed_label),
StatisticsFragment(R.string.statistics_label),
remember(R.string.remember_last_page);
}
- fun drawerPreferencesDialog(context: Context, callback: Runnable?) {
- val hiddenItems = hiddenDrawerItems.map { it.trim() }.toMutableSet()
-// val navTitles = context.resources.getStringArray(R.array.nav_drawer_titles)
- val navTitles = navMap.values.map { context.resources.getString(it.nameRes).trim() }.toTypedArray()
- val checked = BooleanArray(navMap.size)
- for (i in navMap.keys.indices) {
- val tag = navMap.keys.toList()[i]
- if (!hiddenItems.contains(tag)) checked[i] = true
- }
- val builder = MaterialAlertDialogBuilder(context)
- builder.setTitle(R.string.drawer_preferences)
- builder.setMultiChoiceItems(navTitles, checked) { _: DialogInterface?, which: Int, isChecked: Boolean ->
- if (isChecked) hiddenItems.remove(navMap.keys.toList()[which])
- else hiddenItems.add((navMap.keys.toList()[which]).trim())
- }
- builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
- hiddenDrawerItems = hiddenItems.toList()
- if (hiddenItems.contains(defaultPage)) {
- for (tag in navMap.keys) {
- if (!hiddenItems.contains(tag)) {
- defaultPage = tag
- break
- }
- }
- }
- callback?.run()
- }
- builder.setNegativeButton(R.string.cancel_label, null)
- builder.create().show()
- }
+// fun drawerPreferencesDialog(context: Context, callback: Runnable?) {
+// val hiddenItems = hiddenDrawerItems.map { it.trim() }.toMutableSet()
+// val navTitles = navMap.values.map { context.resources.getString(it.nameRes).trim() }.toTypedArray()
+// val checked = BooleanArray(navMap.size)
+// for (i in navMap.keys.indices) {
+// val tag = navMap.keys.toList()[i]
+// if (!hiddenItems.contains(tag)) checked[i] = true
+// }
+// val builder = MaterialAlertDialogBuilder(context)
+// builder.setTitle(R.string.drawer_preferences)
+// builder.setMultiChoiceItems(navTitles, checked) { _: DialogInterface?, which: Int, isChecked: Boolean ->
+// if (isChecked) hiddenItems.remove(navMap.keys.toList()[which])
+// else hiddenItems.add((navMap.keys.toList()[which]).trim())
+// }
+// builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
+// hiddenDrawerItems = hiddenItems.toList()
+// if (hiddenItems.contains(defaultPage)) {
+// for (tag in navMap.keys) {
+// if (!hiddenItems.contains(tag)) {
+// defaultPage = tag
+// break
+// }
+// }
+// }
+// callback?.run()
+// }
+// builder.setNegativeButton(R.string.cancel_label, null)
+// builder.create().show()
+// }
private fun showFullNotificationButtonsDialog() {
val context: Context? = activity
@@ -855,10 +853,10 @@ class PreferenceActivity : AppCompatActivity() {
@Suppress("EnumEntryName")
private enum class Prefs(val res: Int, val tag: String) {
prefSwipeQueue(R.string.queue_label, QueuesFragment.TAG),
- prefSwipeEpisodes(R.string.episodes_label, AllEpisodesFragment.TAG),
- prefSwipeDownloads(R.string.downloads_label, DownloadsFragment.TAG),
+ prefSwipeEpisodes(R.string.episodes_label, EpisodesFragment.TAG),
+// prefSwipeDownloads(R.string.downloads_label, DownloadsFragment.TAG),
prefSwipeFeed(R.string.individual_subscription, FeedEpisodesFragment.TAG),
- prefSwipeHistory(R.string.playback_history_label, HistoryFragment.TAG)
+// prefSwipeHistory(R.string.playback_history_label, HistoryFragment.TAG)
}
}
@@ -1209,32 +1207,145 @@ class PreferenceActivity : AppCompatActivity() {
}
class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
+ private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ exportWithWriter(OpmlWriter(), uri, ExportTypes.OPML)
+ }
- private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
+ private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ exportWithWriter(HtmlWriter(), uri, ExportTypes.HTML)
+ }
- private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
+ private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ exportWithWriter(FavoritesWriter(), uri, ExportTypes.FAVORITES)
+ }
- private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
+ private val chooseProgressExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ exportWithWriter(EpisodesProgressWriter(), uri, ExportTypes.PROGRESS)
+ }
- private val chooseProgressExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.chooseProgressExportPathResult(result) }
+ private val restoreProgressLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data?.data == null) return@registerForActivityResult
+ val uri = result.data!!.data
+ uri?.let {
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ if (isJsonFile(uri)) {
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ val inputStream: InputStream? = requireContext().contentResolver.openInputStream(uri)
+ val reader = BufferedReader(InputStreamReader(inputStream))
+ EpisodeProgressReader.readDocument(reader)
+ reader.close()
+ }
+ withContext(Dispatchers.Main) {
+ showImportSuccessDialog()
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ } else {
+ val context = requireContext()
+ val message = context.getString(R.string.import_file_type_toast) + ".json"
+ showTransportErrorDialog(Throwable(message))
+ }
+ }
+ }
- private val restoreProgressLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.restoreProgressResult(result) }
+ private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult
+ val uri = result.data!!.data
+ uri?.let {
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ if (isRealmFile(uri)) {
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ DatabaseTransporter.importBackup(uri, requireContext())
+ }
+ withContext(Dispatchers.Main) {
+ showImportSuccessDialog()
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ } else {
+ val context = requireContext()
+ val message = context.getString(R.string.import_file_type_toast) + ".realm"
+ showTransportErrorDialog(Throwable(message))
+ }
+ }
+ }
- private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.restoreDatabaseResult(result) }
+ private val backupDatabaseLauncher = registerForActivityResult(BackupDatabase()) { uri: Uri? ->
+ if (uri == null) return@registerForActivityResult
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) { DatabaseTransporter.exportToDocument(uri, requireContext()) }
+ withContext(Dispatchers.Main) {
+ showExportSuccessSnackbar(uri, "application/x-sqlite3")
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ }
- private val backupDatabaseLauncher = registerForActivityResult(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
+ private var showOpmlImportSelectionDialog by mutableStateOf(false)
+ private val readElements = mutableStateListOf()
- private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
- uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
+ private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ if (uri == null) return@registerForActivityResult
+ Logd(TAG, "chooseOpmlImportPathResult: uri: $uri")
+ OpmlTransporter.startImport(requireContext(), uri) {
+ readElements.addAll(it)
+ Logd(TAG, "readElements: ${readElements.size}")
+ }
+// showImportSuccessDialog()
+ showOpmlImportSelectionDialog = true
+ }
- private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.restorePreferencesResult(result) }
+ private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data?.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ if (isPrefDir(uri)) {
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) { PreferencesTransporter.importBackup(uri, requireContext()) }
+ withContext(Dispatchers.Main) {
+ showImportSuccessDialog()
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ } else {
+ val context = requireContext()
+ val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs"
+ showTransportErrorDialog(Throwable(message))
+ }
+ }
private val backupPreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@@ -1243,11 +1354,45 @@ class PreferenceActivity : AppCompatActivity() {
}
}
- private val restoreMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.restoreMediaFilesResult(result) }
+ private val restoreMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data?.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ if (isMediaFilesDir(uri)) {
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) { MediaFilesTransporter.importBackup(uri, requireContext()) }
+ withContext(Dispatchers.Main) {
+ showImportSuccessDialog()
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ } else {
+ val context = requireContext()
+ val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles"
+ showTransportErrorDialog(Throwable(message))
+ }
+ }
- private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- result: ActivityResult -> this.exportMediaFilesResult(result) }
+ private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK || result.data?.data == null) return@registerForActivityResult
+ val uri = result.data!!.data!!
+// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
+// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
+ showProgress = true
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) { MediaFilesTransporter.exportToDocument(uri, requireContext()) }
+ withContext(Dispatchers.Main) {
+ showExportSuccessSnackbar(uri, null)
+ showProgress = false
+ }
+ } catch (e: Throwable) { showTransportErrorDialog(e) }
+ }
+ }
private var showProgress by mutableStateOf(false)
@@ -1273,7 +1418,7 @@ class PreferenceActivity : AppCompatActivity() {
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).verticalScroll(scrollState)) {
Text(stringResource(R.string.database), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
- exportDatabase()
+ backupDatabaseLauncher.launch(dateStampFilename("PodciniBackup-%s.realm"))
})) {
Text(stringResource(R.string.database_export_label), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text(stringResource(R.string.database_export_summary), color = textColor)
@@ -1285,7 +1430,6 @@ class PreferenceActivity : AppCompatActivity() {
Text(stringResource(R.string.database_import_summary), color = textColor)
}
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp))
-
Text(stringResource(R.string.media_files), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 10.dp))
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
exportMediaFiles()
@@ -1300,7 +1444,6 @@ class PreferenceActivity : AppCompatActivity() {
Text(stringResource(R.string.media_files_import_summary), color = textColor)
}
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp))
-
Text(stringResource(R.string.preferences), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 10.dp))
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
exportPreferences()
@@ -1315,7 +1458,6 @@ class PreferenceActivity : AppCompatActivity() {
Text(stringResource(R.string.preferences_import_summary), color = textColor)
}
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp))
-
Text(stringResource(R.string.opml), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 10.dp))
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
openExportPathPicker(ExportTypes.OPML, chooseOpmlExportPathLauncher, OpmlWriter())
@@ -1323,18 +1465,14 @@ class PreferenceActivity : AppCompatActivity() {
Text(stringResource(R.string.opml_export_label), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text(stringResource(R.string.opml_export_summary), color = textColor)
}
+ if (showOpmlImportSelectionDialog) OpmlImportSelectionDialog(readElements) { showOpmlImportSelectionDialog = false }
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
- try {
- chooseOpmlImportPathLauncher.launch("*/*")
- } catch (e: ActivityNotFoundException) {
- Log.e(TAG, "No activity found. Should never happen...")
- }
+ try { chooseOpmlImportPathLauncher.launch("*/*") } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") }
})) {
Text(stringResource(R.string.opml_import_label), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text(stringResource(R.string.opml_import_summary), color = textColor)
}
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp))
-
Text(stringResource(R.string.progress), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 10.dp))
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
openExportPathPicker(ExportTypes.PROGRESS, chooseProgressExportPathLauncher, EpisodesProgressWriter())
@@ -1349,7 +1487,6 @@ class PreferenceActivity : AppCompatActivity() {
Text(stringResource(R.string.progress_import_summary), color = textColor)
}
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp))
-
Text(stringResource(R.string.html), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 10.dp))
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
openExportPathPicker(ExportTypes.HTML, chooseHtmlExportPathLauncher, HtmlWriter())
@@ -1392,9 +1529,7 @@ class PreferenceActivity : AppCompatActivity() {
val worker = DocumentFileExportWorker(exportWriter, context!!, uri)
try {
val output = worker.exportFile()
- withContext(Dispatchers.Main) {
- showExportSuccessSnackbar(output.uri, exportType.contentType)
- }
+ withContext(Dispatchers.Main) { showExportSuccessSnackbar(output.uri, exportType.contentType) }
} catch (e: Exception) { showTransportErrorDialog(e)
} finally { showProgress = false }
}
@@ -1451,10 +1586,6 @@ class PreferenceActivity : AppCompatActivity() {
builder.show()
}
- private fun exportDatabase() {
- backupDatabaseLauncher.launch(dateStampFilename("PodciniBackup-%s.realm"))
- }
-
private fun importDatabase() {
// setup the alert builder
val builder = MaterialAlertDialogBuilder(requireActivity())
@@ -1519,100 +1650,11 @@ class PreferenceActivity : AppCompatActivity() {
builder.show()
}
- private fun chooseProgressExportPathResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- exportWithWriter(EpisodesProgressWriter(), uri, ExportTypes.PROGRESS)
- }
-
- private fun chooseOpmlExportPathResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- exportWithWriter(OpmlWriter(), uri, ExportTypes.OPML)
- }
-
- private fun chooseHtmlExportPathResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- exportWithWriter(HtmlWriter(), uri, ExportTypes.HTML)
- }
-
- private fun chooseFavoritesExportPathResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- exportWithWriter(FavoritesWriter(), uri, ExportTypes.FAVORITES)
- }
-
- private fun restoreProgressResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data?.data == null) return
- val uri = result.data!!.data
- uri?.let {
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- if (isJsonFile(uri)) {
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- val inputStream: InputStream? = requireContext().contentResolver.openInputStream(uri)
- val reader = BufferedReader(InputStreamReader(inputStream))
- EpisodeProgressReader.readDocument(reader)
- reader.close()
- }
- withContext(Dispatchers.Main) {
- showImportSuccessDialog()
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- } else {
- val context = requireContext()
- val message = context.getString(R.string.import_file_type_toast) + ".json"
- showTransportErrorDialog(Throwable(message))
- }
- }
- }
-
private fun isJsonFile(uri: Uri): Boolean {
val fileName = uri.lastPathSegment ?: return false
return fileName.endsWith(".json", ignoreCase = true)
}
- private fun restoreDatabaseResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data == null) return
- val uri = result.data!!.data
- uri?.let {
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- if (isRealmFile(uri)) {
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- DatabaseTransporter.importBackup(uri, requireContext())
- }
- withContext(Dispatchers.Main) {
- showImportSuccessDialog()
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- } else {
- val context = requireContext()
- val message = context.getString(R.string.import_file_type_toast) + ".realm"
- showTransportErrorDialog(Throwable(message))
- }
- }
- }
-
private fun isRealmFile(uri: Uri): Boolean {
val fileName = uri.lastPathSegment ?: return false
return fileName.endsWith(".realm", ignoreCase = true)
@@ -1628,101 +1670,6 @@ class PreferenceActivity : AppCompatActivity() {
return fileName.contains("Podcini-MediaFiles", ignoreCase = true)
}
- private fun restorePreferencesResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data?.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- if (isPrefDir(uri)) {
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- PreferencesTransporter.importBackup(uri, requireContext())
- }
- withContext(Dispatchers.Main) {
- showImportSuccessDialog()
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- } else {
- val context = requireContext()
- val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs"
- showTransportErrorDialog(Throwable(message))
- }
- }
-
- private fun restoreMediaFilesResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data?.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- if (isMediaFilesDir(uri)) {
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- MediaFilesTransporter.importBackup(uri, requireContext())
- }
- withContext(Dispatchers.Main) {
- showImportSuccessDialog()
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- } else {
- val context = requireContext()
- val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles"
- showTransportErrorDialog(Throwable(message))
- }
- }
-
- private fun exportMediaFilesResult(result: ActivityResult) {
- if (result.resultCode != RESULT_OK || result.data?.data == null) return
- val uri = result.data!!.data!!
-// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
-// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- MediaFilesTransporter.exportToDocument(uri, requireContext())
- }
- withContext(Dispatchers.Main) {
- showExportSuccessSnackbar(uri, null)
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- }
-
- private fun backupDatabaseResult(uri: Uri?) {
- if (uri == null) return
- showProgress = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- DatabaseTransporter.exportToDocument(uri, requireContext())
- }
- withContext(Dispatchers.Main) {
- showExportSuccessSnackbar(uri, "application/x-sqlite3")
- showProgress = false
- }
- } catch (e: Throwable) { showTransportErrorDialog(e) }
- }
- }
-
- private fun chooseOpmlImportPathResult(uri: Uri?) {
- if (uri == null) return
- Logd(TAG, "chooseOpmlImportPathResult: uri: $uri")
-// OpmlTransporter.startImport(requireContext(), uri)
-// showImportSuccessDialog()
- val intent = Intent(context, OpmlImportActivity::class.java)
- intent.setData(uri)
- startActivity(intent)
- }
-
private fun openExportPathPicker(exportType: ExportTypes, result: ActivityResultLauncher, writer: ExportWriter) {
val title = dateStampFilename(exportType.outputNameTemplate)
@@ -1745,9 +1692,7 @@ class PreferenceActivity : AppCompatActivity() {
private class BackupDatabase : CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
- return super.createIntent(context, input)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("application/x-sqlite3")
+ return super.createIntent(context, input).addCategory(Intent.CATEGORY_OPENABLE).setType("application/x-sqlite3")
}
}
@@ -1837,9 +1782,7 @@ class PreferenceActivity : AppCompatActivity() {
}
}
when {
-// for debug version importing release version
BuildConfig.DEBUG && !destName.contains(".debug") -> destName = destName.replace("podcini.R", "podcini.R.debug")
-// for release version importing debug version
!BuildConfig.DEBUG && destName.contains(".debug") -> destName = destName.replace(".debug", "")
}
val destFile = File(sharedPreferencesDir, destName)
@@ -1865,9 +1808,7 @@ class PreferenceActivity : AppCompatActivity() {
val mediaDir = context.getExternalFilesDir("media") ?: return
val chosenDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Destination directory is not valid")
val exportSubDir = chosenDir.createDirectory("Podcini-MediaFiles") ?: throw IOException("Error creating subdirectory Podcini-Prefs")
- mediaDir.listFiles()?.forEach { file ->
- copyRecursive(context, file, mediaDir, exportSubDir)
- }
+ mediaDir.listFiles()?.forEach { file -> copyRecursive(context, file, mediaDir, exportSubDir) }
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
@@ -1879,9 +1820,7 @@ class PreferenceActivity : AppCompatActivity() {
val dirFiles = srcFile.listFiles()
if (!dirFiles.isNullOrEmpty()) {
val destDir = destRootDir.findFile(relativePath) ?: destRootDir.createDirectory(relativePath) ?: return
- dirFiles.forEach { file ->
- copyRecursive(context, file, srcFile, destDir)
- }
+ dirFiles.forEach { file -> copyRecursive(context, file, srcFile, destDir) }
}
} else {
val destFile = destRootDir.createFile("application/octet-stream", relativePath) ?: return
@@ -1907,14 +1846,10 @@ class PreferenceActivity : AppCompatActivity() {
feed = nameFeedMap[relativePath] ?: return
Logd(TAG, "copyRecursive found feed: ${feed?.title}")
nameEpisodeMap.clear()
- feed!!.episodes.forEach { e ->
- if (!e.title.isNullOrEmpty()) nameEpisodeMap[generateFileName(e.title!!)] = e
- }
+ feed!!.episodes.forEach { e -> if (!e.title.isNullOrEmpty()) nameEpisodeMap[generateFileName(e.title!!)] = e }
val destFile = File(destRootDir, relativePath)
if (!destFile.exists()) destFile.mkdirs()
- srcFile.listFiles().forEach { file ->
- copyRecursive(context, file, srcFile, destFile)
- }
+ srcFile.listFiles().forEach { file -> copyRecursive(context, file, srcFile, destFile) }
} else {
val nameParts = relativePath.split(".")
if (nameParts.size < 3) return
@@ -1950,9 +1885,7 @@ class PreferenceActivity : AppCompatActivity() {
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
- while (inputStream.read(buffer).also { bytesRead = it } != -1) {
- outputStream.write(buffer, 0, bytesRead)
- }
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) outputStream.write(buffer, 0, bytesRead)
}
@Throws(IOException::class)
fun importBackup(uri: Uri, context: Context) {
@@ -1963,12 +1896,8 @@ class PreferenceActivity : AppCompatActivity() {
val fileList = exportedDir.listFiles()
if (fileList.isNotEmpty()) {
val feeds = getFeedList()
- feeds.forEach { f ->
- if (!f.title.isNullOrEmpty()) nameFeedMap[generateFileName(f.title!!)] = f
- }
- fileList.forEach { file ->
- copyRecursive(context, file, exportedDir, mediaDir)
- }
+ feeds.forEach { f -> if (!f.title.isNullOrEmpty()) nameFeedMap[generateFileName(f.title!!)] = f }
+ fileList.forEach { file -> copyRecursive(context, file, exportedDir, mediaDir) }
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
@@ -2094,7 +2023,7 @@ class PreferenceActivity : AppCompatActivity() {
val queuedEpisodeActions: MutableList = mutableListOf()
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD)
- val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
+ val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.superb.name), EpisodeSortOrder.DATE_NEW_OLD)
val comItems = mutableSetOf()
comItems.addAll(pausedItems)
comItems.addAll(readItems)
@@ -2153,7 +2082,7 @@ class PreferenceActivity : AppCompatActivity() {
val favTemplate = IOUtils.toString(favTemplateStream, UTF_8)
val feedTemplateStream = context.assets.open(FEED_TEMPLATE)
val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8)
- val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
+ val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.superb.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoritesByFeed = buildFeedMap(allFavorites)
writer!!.append(templateParts[0])
for (feedId in favoritesByFeed.keys) {
@@ -2724,7 +2653,6 @@ class PreferenceActivity : AppCompatActivity() {
}
class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
-
var selectedProvider by mutableStateOf(SynchronizationProviderViewData.fromIdentifier(selectedSyncProviderKey))
var loggedIn by mutableStateOf(isProviderConnected)
@@ -3153,7 +3081,7 @@ class PreferenceActivity : AppCompatActivity() {
login.isEnabled = false
progressBar.visibility = View.VISIBLE
txtvError.visibility = View.GONE
- val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ val inputManager = requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
lifecycleScope.launch {
@@ -3442,7 +3370,7 @@ class PreferenceActivity : AppCompatActivity() {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefShowDownloadReport.name, it).apply()
})
}
- if (SynchronizationSettings.isProviderConnected) {
+ if (isProviderConnected) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.notification_channel_sync_error), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
index d93037cb..12583bda 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
@@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.ShareLog
-import ac.mdiq.podcini.ui.compose.ConfirmAddYoutubeEpisode
+import ac.mdiq.podcini.ui.compose.ConfirmAddYoutubeEpisode1
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.Logd
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
@@ -52,7 +52,7 @@ class ShareReceiverActivity : AppCompatActivity() {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(this) {
- ConfirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
+ ConfirmAddYoutubeEpisode1(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
showDialog.value = false
finish()
})
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 c909307e..c59d50ba 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,16 +1,22 @@
package ac.mdiq.podcini.ui.compose
+import ac.mdiq.podcini.R
+import ac.mdiq.podcini.preferences.UserPreferences
+import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
@@ -165,3 +171,49 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
}
}
}
+
+@Composable
+fun SimpleSwitchDialog(title: String, text: String, onDismissRequest: ()->Unit, callback: (Boolean)-> Unit) {
+ val textColor = MaterialTheme.colorScheme.onSurface
+ var isChecked by remember { mutableStateOf(false) }
+ AlertDialog(onDismissRequest = { onDismissRequest() },
+ title = { Text(title, style = MaterialTheme.typography.titleLarge) },
+ text = {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
+ Text(text, color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
+ Switch(checked = isChecked, onCheckedChange = { isChecked = it })
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ callback(isChecked)
+ onDismissRequest()
+ }) { Text(text = "OK") }
+ },
+ dismissButton = { TextButton(onClick = { onDismissRequest() }) { Text(text = "Cancel") } }
+ )
+}
+
+@Composable
+fun TitleSummaryColumn(titleRes: Int, summaryRes: Int, callback: ()-> Unit) {
+ val textColor = MaterialTheme.colorScheme.onSurface
+ Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = { callback() })) {
+ Text(stringResource(titleRes), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ Text(stringResource(summaryRes), color = textColor)
+ }
+}
+
+@Composable
+fun TitleSummarySwitchRow(titleRes: Int, summaryRes: Int, prefName: String) {
+ val textColor = MaterialTheme.colorScheme.onSurface
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(titleRes), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ Text(stringResource(summaryRes), color = textColor)
+ }
+ var isChecked by remember { mutableStateOf(appPrefs.getBoolean(prefName, false)) }
+ Switch(checked = isChecked, onCheckedChange = {
+ isChecked = it
+ appPrefs.edit().putBoolean(prefName, it).apply() })
+ }
+}
\ No newline at end of file
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 bd3e90d4..c4054f81 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
@@ -35,6 +35,8 @@ 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.database.Episodes.hasAlmostEnded
+import ac.mdiq.podcini.storage.database.Feeds.addToSyndicate
+import ac.mdiq.podcini.storage.database.Feeds.createYTSyndicates
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
import ac.mdiq.podcini.ui.actions.NullActionButton
@@ -71,6 +73,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
+import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -98,6 +101,7 @@ import androidx.documentfile.provider.DocumentFile
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
+import com.skydoves.balloon.textForm
import io.realm.kotlin.notifications.SingleQueryChange
import io.realm.kotlin.notifications.UpdatedObject
import kotlinx.coroutines.*
@@ -341,9 +345,7 @@ fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val scrollState = rememberScrollState()
- Column(modifier = Modifier
- .verticalScroll(scrollState)
- .padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
+ Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
var removeChecked by remember { mutableStateOf(false) }
var toFeed by remember { mutableStateOf(null) }
if (synthetics.isNotEmpty()) {
@@ -455,7 +457,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
val showConfirmYoutubeDialog = remember { mutableStateOf(false) }
val youtubeUrls = remember { mutableListOf() }
- ConfirmAddYoutubeEpisode(youtubeUrls, showConfirmYoutubeDialog.value, onDismissRequest = { showConfirmYoutubeDialog.value = false })
+ ConfirmAddYoutubeEpisode1(youtubeUrls, showConfirmYoutubeDialog.value, onDismissRequest = { showConfirmYoutubeDialog.value = false })
var showChooseRatingDialog by remember { mutableStateOf(false) }
if (showChooseRatingDialog) ChooseRatingDialog(selected) { showChooseRatingDialog = false }
@@ -581,14 +583,12 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
for (e in selected) {
Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}")
val url = URL(e.media?.downloadUrl ?: "")
- if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) {
+ if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url))
youtubeUrls.add(e.media!!.downloadUrl!!)
- } else addToMiscSyndicate(e)
+ else addToMiscSyndicate(e)
}
Logd(TAG, "youtubeUrls: ${youtubeUrls.size}")
- withContext(Dispatchers.Main) {
- showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty()
- }
+ withContext(Dispatchers.Main) { showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty() }
}
}, verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.AddCircle, "Reserve episodes")
@@ -899,32 +899,56 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed:
}
@Composable
-fun ConfirmAddYoutubeEpisode(sharedUrls: List, showDialog: Boolean, onDismissRequest: () -> Unit) {
+fun ConfirmAddYoutubeEpisode1(sharedUrls: List, showDialog: Boolean, onDismissRequest: () -> Unit) {
val TAG = "confirmAddEpisode"
var showToast by remember { mutableStateOf(false) }
var toastMassege by remember { mutableStateOf("")}
if (showToast) CustomToast(message = toastMassege, onDismiss = { showToast = false })
if (showDialog) {
+ val YTSyndMap = remember { mutableStateMapOf() }
+ val synthetics = remember { realm.query(Feed::class).query("id >= 1 && id <= 1000").find().toMutableStateList() }
Dialog(onDismissRequest = { onDismissRequest() }) {
- Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
- Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
- var audioOnly by remember { mutableStateOf(false) }
- Row(Modifier.fillMaxWidth()) {
- Checkbox(checked = audioOnly, onCheckedChange = { audioOnly = it })
- Text(text = stringResource(R.string.pref_video_mode_audio_only), style = MaterialTheme.typography.bodyLarge.merge())
+ val textColor = MaterialTheme.colorScheme.onSurface
+ Card(modifier = Modifier.height(350.dp).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
+ var toFeed by remember { mutableStateOf(null) }
+ var showComfirmButton by remember { mutableStateOf(toFeed != null) }
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text(stringResource(R.string.add_to_feed), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ if (YTSyndMap.size < 4) {
+ Button(onClick = {
+ createYTSyndicates()
+ synthetics.clear()
+ synthetics.addAll(realm.query(Feed::class).query("id >= 1 && id <= 1000").find())
+ }) { Text(stringResource(R.string.create_YT_syndicates)) }
}
- var showComfirmButton by remember { mutableStateOf(true) }
+ if (synthetics.isNotEmpty()) {
+ LazyColumn(modifier = Modifier.weight(1f).padding(start = 10.dp, end = 10.dp), verticalArrangement = Arrangement.Center) {
+ items(synthetics.size) { index ->
+ val f = synthetics[index]
+ if (f.id <= 4) YTSyndMap[f.id.toInt()] = true
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(selected = toFeed == f, onClick = {
+ toFeed = f
+ showComfirmButton = true
+ })
+ Text(f.title ?: "No title", color = textColor)
+ }
+ }
+ }
+ } else Text(text = stringResource(R.string.create_synthetic_first_note), color = textColor)
+ var showProgress by remember { mutableStateOf(false) }
if (showComfirmButton) {
Button(onClick = {
showComfirmButton = false
+ showProgress = true
CoroutineScope(Dispatchers.IO).launch {
for (url in sharedUrls) {
val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
try {
val info = StreamInfo.getInfo(Vista.getService(0), url)
val episode = episodeFromStreamInfo(info)
- val status = addToYoutubeSyndicate(episode, !audioOnly)
+ val status = addToSyndicate(episode, toFeed!!)
if (log != null) upsert(log) {
it.title = episode.title
it.status = status
@@ -932,14 +956,15 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List, showDialog: Boolean, onDi
} catch (e: Throwable) {
toastMassege = "Receive share error: ${e.message}"
Log.e(TAG, toastMassege)
- if (log != null) upsert(log) { it.details = e.message?: "error" }
+ if (log != null) upsert(log) { it.details = e.message ?: "error" }
withContext(Dispatchers.Main) { showToast = true }
}
}
withContext(Dispatchers.Main) { onDismissRequest() }
}
}) { Text("Confirm") }
- } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
+ }
+ if (showProgress) CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 40.dp, end = 40.dp).width(30.dp).height(30.dp))
}
}
}
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 4a5ad136..0e244f84 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
@@ -2,6 +2,7 @@ package ac.mdiq.podcini.ui.compose
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.feed.FeedBuilder
+import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
@@ -10,19 +11,19 @@ import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.prefPlaybackSpeed
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
+import ac.mdiq.podcini.preferences.OpmlTransporter
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.storage.database.Feeds.buildTags
import ac.mdiq.podcini.storage.database.Feeds.createSynthetic
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
-import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags
+import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.RealmDB.realm
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.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
@@ -33,8 +34,11 @@ import ac.mdiq.podcini.util.MiscFormatter
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import android.util.Log
import android.view.Gravity
+import android.widget.Toast
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@@ -44,6 +48,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
+import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -69,6 +74,7 @@ import coil.request.ImageRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import java.text.DecimalFormat
@@ -589,3 +595,58 @@ fun PlaybackSpeedFullDialog(settingCode: BooleanArray, indexDefault: Int, maxSpe
}
}
}
+
+@Composable
+fun OpmlImportSelectionDialog(readElements: SnapshotStateList, onDismissRequest: () -> Unit) {
+ val context = LocalContext.current
+ val selectedItems = remember { mutableStateMapOf() }
+ AlertDialog(onDismissRequest = { onDismissRequest() },
+ title = { Text("Import OPML file") },
+ text = {
+ var isSelectAllChecked by remember { mutableStateOf(false) }
+ Column(modifier = Modifier.fillMaxSize()) {
+ Row(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "Select/Deselect All", modifier = Modifier.weight(1f))
+ Checkbox(checked = isSelectAllChecked, onCheckedChange = { isChecked ->
+ isSelectAllChecked = isChecked
+ readElements.forEachIndexed { index, _ -> selectedItems.put(index, isChecked) }
+ })
+ }
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ itemsIndexed(readElements) { index, item ->
+ Row(modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = item.text?:"", modifier = Modifier.weight(1f))
+ Checkbox(checked = selectedItems[index]?: false, onCheckedChange = { checked -> selectedItems.put(index, checked) })
+ }
+ }
+ }
+ }
+ },
+ confirmButton = {
+ Button(onClick = {
+ Logd("OpmlImportSelectionDialog", "checked: $selectedItems")
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ withContext(Dispatchers.IO) {
+ if (readElements.isNotEmpty()) {
+ for (i in selectedItems.keys) {
+ if (selectedItems[i] != true) continue
+ val element = readElements[i]
+ val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast")
+ feed.episodes.clear()
+ updateFeed(context, feed, false)
+ }
+ runOnce(context)
+ }
+ }
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ Toast.makeText(context, (e.message ?: "Import error"), Toast.LENGTH_LONG).show()
+ }
+ }
+ onDismissRequest()
+ }) { Text("Confirm") }
+ },
+ dismissButton = { Button(onClick = { onDismissRequest() }) { Text("Dismiss") } }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt
deleted file mode 100644
index 95b6ec83..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package ac.mdiq.podcini.ui.fragment
-
-import ac.mdiq.podcini.R
-import ac.mdiq.podcini.preferences.UserPreferences
-import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
-import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
-import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
-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.util.Logd
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import org.apache.commons.lang3.StringUtils
-
-
-class AllEpisodesFragment : BaseEpisodesFragment() {
- private var allEpisodes: List = listOf()
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- val root = super.onCreateView(inflater, container, savedInstanceState)
- Logd(TAG, "fragment onCreateView")
-
- toolbar.inflateMenu(R.menu.episodes)
- toolbar.setTitle(R.string.episodes_label)
- sortOrder = allEpisodesSortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
- updateToolbar()
-// txtvInformation.setOnClickListener {
-// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
-// }
- return root
- }
-
- override fun onDestroyView() {
- allEpisodes = listOf()
- super.onDestroyView()
- }
-
- private var loadItemsRunning = false
- override fun loadData(): List {
- val filter = getFilter()
- if (!loadItemsRunning) {
- loadItemsRunning = true
- allEpisodes = getEpisodes(0, Int.MAX_VALUE, filter, allEpisodesSortOrder, false)
- Logd(TAG, "loadData ${allEpisodes.size}")
- loadItemsRunning = false
- }
- if (allEpisodes.isEmpty()) return listOf()
-// allEpisodes = allEpisodes.filter { filter.matchesForQueues(it) }
- return allEpisodes
- }
-
- override fun loadTotalItemCount(): Int {
- return getEpisodesCount(getFilter())
- }
-
- override fun getFilter(): EpisodeFilter {
- return EpisodeFilter(prefFilterAllEpisodes)
- }
-
- override fun getPrefName(): String {
- return PREF_NAME
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onOptionsItemSelected(item)) return true
-
- when (item.itemId) {
- R.id.filter_items -> showFilterDialog = true
- R.id.episodes_sort -> showSortDialog = true
- else -> return false
- }
- return true
- }
-
- override fun updateToolbar() {
- swipeActions.setFilter(getFilter())
- var info = "${episodes.size} episodes"
- if (getFilter().properties.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
- infoBarText.value = info
- }
-
- override fun onFilterChanged(filterValues: Set) {
- prefFilterAllEpisodes = StringUtils.join(filterValues, ",")
- page = 1
- loadItems()
- }
-
- override fun onSort(order: EpisodeSortOrder) {
- allEpisodesSortOrder = order
- page = 1
- loadItems()
- }
-
- companion object {
- val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous"
- const val PREF_NAME: String = "PrefAllEpisodesFragment"
- var allEpisodesSortOrder: EpisodeSortOrder?
- get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(UserPreferences.Prefs.prefEpisodesSort.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
- set(s) {
- appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesSort.name, "" + s!!.code).apply()
- }
- var prefFilterAllEpisodes: String
- get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodesFilter.name, "")?:""
- set(filter) {
- appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesFilter.name, filter).apply()
- }
- }
-}
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 f3713e31..e6354d85 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
@@ -7,6 +7,7 @@ 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.ui.actions.EpisodeActionButton
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
@@ -24,7 +25,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
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
@@ -37,13 +37,11 @@ import kotlinx.coroutines.withContext
abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val TAG = this::class.simpleName ?: "Anonymous"
- @JvmField
- protected var page: Int = 1
- private var displayUpArrow = false
-
var _binding: ComposeFragmentBinding? = null
protected val binding get() = _binding!!
+ private var displayUpArrow = false
+
protected var infoBarText = mutableStateOf("")
private var leftActionState = mutableStateOf(NoActionSwipeAction())
private var rightActionState = mutableStateOf(NoActionSwipeAction())
@@ -52,11 +50,13 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
lateinit var swipeActions: SwipeActions
val episodes = mutableListOf()
- private val vms = mutableStateListOf()
+ protected val vms = mutableStateListOf()
var showFilterDialog by mutableStateOf(false)
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
+ var actionButtonToPass by mutableStateOf<((Episode) -> EpisodeActionButton)?>(null)
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
@@ -65,24 +65,17 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
toolbar = binding.toolbar
toolbar.setOnMenuItemClickListener(this)
-// toolbar.setOnLongClickListener {
-// recyclerView.scrollToPosition(5)
-// recyclerView.post { recyclerView.smoothScrollToPosition(0) }
-// false
-// }
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
-// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
-// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
-
swipeActions = SwipeActions(this, TAG)
lifecycle.addObserver(swipeActions)
binding.mainView.setContent {
CustomTheme(requireContext()) {
- if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) { onFilterChanged(it) }
+ if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), filtersDisabled = filtersDisabled(),
+ onDismissRequest = { showFilterDialog = false } ) { onFilterChanged(it) }
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ -> onSort(order) }
Column {
@@ -91,24 +84,29 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
activity as MainActivity, vms = vms,
leftSwipeCB = {
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else leftActionState.value.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
+ else leftActionState.value.performAction(it, this@BaseEpisodesFragment)
},
rightSwipeCB = {
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else rightActionState.value.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
+ else rightActionState.value.performAction(it, this@BaseEpisodesFragment)
},
+ actionButton_ = actionButtonToPass
)
}
}
}
- swipeActions.setFilter(getFilter())
+// swipeActions.setFilter(getFilter())
refreshSwipeTelltale()
return binding.root
}
open fun onFilterChanged(filterValues: Set) {}
+ open fun filtersDisabled(): MutableSet {
+ return mutableSetOf()
+ }
+
open fun onSort(order: EpisodeSortOrder) {}
override fun onStart() {
@@ -122,15 +120,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
cancelFlowEvents()
}
-// override fun onPause() {
-// super.onPause()
-//// recyclerView.saveScrollPosition(getPrefName())
-//// unregisterForContextMenu(recyclerView)
-// }
-
- @Deprecated("Deprecated in Java")
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (super.onOptionsItemSelected(item)) return true
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+// if (super.onMenuItemClick(item)) return true
val itemId = item.itemId
when (itemId) {
R.id.action_search -> {
@@ -164,7 +155,6 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
// if (!event.isCompleted(url)) continue
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
}
}
@@ -187,6 +177,9 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
+ is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
+ is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event)
+ is FlowEvent.HistoryEvent -> onHistoryEvent(event)
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent, is FlowEvent.RatingEvent -> loadItems()
else -> {}
}
@@ -210,6 +203,12 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
}
}
+ protected open fun onHistoryEvent(event: FlowEvent.HistoryEvent) {}
+
+ protected open fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) { }
+
+ protected open fun onEpisodeMediaEvent(event: FlowEvent.EpisodeMediaEvent) {}
+
private fun refreshSwipeTelltale() {
leftActionState.value = swipeActions.actions.left[0]
rightActionState.value = swipeActions.actions.right[0]
@@ -222,14 +221,13 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
Logd(TAG, "loadItems() called")
lifecycleScope.launch {
try {
- val data = withContext(Dispatchers.IO) { Pair(loadData().toMutableList(), loadTotalItemCount()) }
- val restoreScrollPosition = episodes.isEmpty()
- episodes.clear()
- episodes.addAll(data.first)
+ withContext(Dispatchers.IO) {
+ episodes.clear()
+ episodes.addAll(loadData())
+ }
withContext(Dispatchers.Main) {
vms.clear()
- for (e in data.first) { vms.add(EpisodeVM(e)) }
-// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
+ for (e in episodes) { vms.add(EpisodeVM(e)) }
updateToolbar()
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
@@ -240,8 +238,6 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
protected abstract fun loadData(): List
- protected abstract fun loadTotalItemCount(): Int
-
open fun getFilter(): EpisodeFilter {
return EpisodeFilter.unfiltered()
}
@@ -257,6 +253,5 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
companion object {
private const val KEY_UP_ARROW = "up_arrow"
- const val EPISODES_PER_PAGE: Int = 50
}
}
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
deleted file mode 100644
index b55d8d1a..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt
+++ /dev/null
@@ -1,426 +0,0 @@
-package ac.mdiq.podcini.ui.fragment
-
-import ac.mdiq.podcini.R
-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
-import ac.mdiq.podcini.storage.database.RealmDB.upsert
-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.EpisodeMedia
-import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-import ac.mdiq.podcini.ui.actions.DeleteActionButton
-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.util.EventFlow
-import ac.mdiq.podcini.util.FlowEvent
-import ac.mdiq.podcini.util.Logd
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.appcompat.widget.Toolbar
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import com.google.android.material.appbar.MaterialToolbar
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.apache.commons.lang3.StringUtils
-import java.io.File
-import java.util.*
-
-/**
- * Displays all completed downloads and provides a button to delete them.
- */
- class DownloadsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
-
- private var _binding: ComposeFragmentBinding? = null
- private val binding get() = _binding!!
-
- private var runningDownloads: Set = HashSet()
- private val episodes = mutableListOf()
- private val vms = mutableStateListOf()
-
- private var infoBarText = mutableStateOf("")
- private var leftActionState = mutableStateOf(NoActionSwipeAction())
- private var rightActionState = mutableStateOf(NoActionSwipeAction())
- var showFilterDialog by mutableStateOf(false)
- var showSortDialog by mutableStateOf(false)
- var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
-
- private lateinit var toolbar: MaterialToolbar
- private lateinit var swipeActions: SwipeActions
-
- private var displayUpArrow = false
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- _binding = ComposeFragmentBinding.inflate(inflater)
-
- sortOrder = downloadsSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
-
- Logd(TAG, "fragment onCreateView")
- toolbar = binding.toolbar
- toolbar.setTitle(R.string.downloads_label)
- toolbar.inflateMenu(R.menu.downloads_completed)
- toolbar.setOnMenuItemClickListener(this)
-// toolbar.setOnLongClickListener {
-//// recyclerView.scrollToPosition(5)
-//// recyclerView.post { recyclerView.smoothScrollToPosition(0) }
-// false
-// }
- displayUpArrow = parentFragmentManager.backStackEntryCount != 0
- if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
- (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
-
- swipeActions = SwipeActions(this, TAG)
- swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
- binding.mainView.setContent {
- CustomTheme(requireContext()) {
- if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(),
- filtersDisabled = mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA),
- onDismissRequest = { showFilterDialog = false } ) {
-// EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(it))
- val fSet = it.toMutableSet()
- fSet.add(EpisodeFilter.States.downloaded.name)
- prefFilterDownloads = StringUtils.join(fSet, ",")
- Logd(TAG, "onFilterChanged: $prefFilterDownloads")
- loadItems()
- }
- if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ ->
- downloadsSortedOrder = order
- loadItems()
- }
-
- Column {
- InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
- EpisodeLazyColumn(activity as MainActivity, vms = vms,
- leftSwipeCB = {
- if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else leftActionState.value.performAction(it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
- },
- rightSwipeCB = {
- if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else rightActionState.value.performAction(it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
- },
- actionButton_ = { DeleteActionButton(it) })
- }
- }
- }
-// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
-// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
-
- lifecycle.addObserver(swipeActions)
- refreshSwipeTelltale()
-// if (arguments != null && requireArguments().getBoolean(ARG_SHOW_LOGS, false)) DownloadLogFragment().show(childFragmentManager, null)
-
-// addEmptyView()
- return binding.root
- }
-
- override fun onStart() {
- super.onStart()
- procFlowEvents()
- loadItems()
- }
-
- override fun onStop() {
- super.onStop()
- cancelFlowEvents()
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- outState.putBoolean(KEY_UP_ARROW, displayUpArrow)
- super.onSaveInstanceState(outState)
- }
-
- override fun onDestroyView() {
- Logd(TAG, "onDestroyView")
- _binding = null
- toolbar.setOnMenuItemClickListener(null)
- toolbar.setOnLongClickListener(null)
- episodes.clear()
- vms.clear()
- super.onDestroyView()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.filter_items -> showFilterDialog = true
- R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
- R.id.downloads_sort -> showSortDialog = true
- R.id.reconcile -> reconcile()
- else -> return false
- }
- return true
- }
-
- private fun getFilter(): EpisodeFilter {
- return EpisodeFilter(prefFilterDownloads)
- }
-
- private val nameEpisodeMap: MutableMap = mutableMapOf()
- private val filesRemoved: MutableList = mutableListOf()
- private fun reconcile() {
- runOnIOScope {
- val items = realm.query(Episode::class).query("media.episode == nil").find()
- Logd(TAG, "number of episode with null backlink: ${items.size}")
- for (item in items) {
- if (item.media != null ) upsert(item) { it.media!!.episode = it }
- }
- nameEpisodeMap.clear()
- for (e in episodes) {
- var fileUrl = e.media?.fileUrl ?: continue
- fileUrl = fileUrl.substring(fileUrl.lastIndexOf('/') + 1)
- Logd(TAG, "reconcile: fileUrl: $fileUrl")
- nameEpisodeMap[fileUrl] = e
- }
- val mediaDir = requireContext().getExternalFilesDir("media") ?: return@runOnIOScope
- mediaDir.listFiles()?.forEach { file -> traverse(file, mediaDir) }
- Logd(TAG, "reconcile: end, episodes missing file: ${nameEpisodeMap.size}")
- if (nameEpisodeMap.isNotEmpty()) {
- for (e in nameEpisodeMap.values) {
- upsertBlk(e) { it.media?.setfileUrlOrNull(null) }
- }
- }
- loadItems()
- Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}")
- withContext(Dispatchers.Main) {
- Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show()
- }
- }
- }
-
- private fun traverse(srcFile: File, srcRootDir: File) {
- val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1)
- if (srcFile.isDirectory) {
- Logd(TAG, "traverse folder title: $relativePath")
- val dirFiles = srcFile.listFiles()
- dirFiles?.forEach { file -> traverse(file, srcFile) }
- } else {
- Logd(TAG, "traverse: $srcFile")
- val episode = nameEpisodeMap.remove(relativePath)
- if (episode == null) {
- Logd(TAG, "traverse: error: episode not exist in map: $relativePath")
- filesRemoved.add(relativePath)
- srcFile.delete()
- return
- }
- Logd(TAG, "traverse found episode: ${episode.title}")
- }
- }
-
- private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
- val newRunningDownloads: MutableSet = HashSet()
- for (url in event.urls) {
- if (DownloadServiceInterface.get()?.isDownloadingEpisode(url) == true) newRunningDownloads.add(url)
- }
- if (newRunningDownloads != runningDownloads) {
- runningDownloads = newRunningDownloads
- loadItems()
- return // Refreshed anyway
- }
-// for (downloadUrl in event.urls) {
-// val pos = Episodes.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
-// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
-// }
- }
-
- private var eventSink: Job? = null
- private var eventStickySink: Job? = null
- private fun cancelFlowEvents() {
- eventSink?.cancel()
- eventSink = null
- eventStickySink?.cancel()
- eventStickySink = null
- }
- private fun procFlowEvents() {
- if (eventSink == null) eventSink = lifecycleScope.launch {
- EventFlow.events.collectLatest { event ->
- Logd(TAG, "Received event: ${event.TAG}")
- when (event) {
- is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
-// is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event)
- is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event)
- is FlowEvent.PlayerSettingsEvent -> loadItems()
- is FlowEvent.DownloadLogEvent -> loadItems()
- is FlowEvent.QueueEvent -> loadItems()
- is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
- is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
- else -> {}
- }
- }
- }
-// if (eventStickySink == null) eventStickySink = lifecycleScope.launch {
-// EventFlow.stickyEvents.collectLatest { event ->
-// Logd(TAG, "Received sticky event: ${event.TAG}")
-// when (event) {
-// is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
-// else -> {}
-// }
-// }
-// }
- }
-
-// private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) {
-// val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf()
-// fSet.add(EpisodeFilter.States.downloaded.name)
-// prefFilterDownloads = StringUtils.join(fSet, ",")
-// Logd(TAG, "onFilterChanged: $prefFilterDownloads")
-// loadItems()
-// }
-
- private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
-// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
- var i = 0
- val size: Int = event.episodes.size
- while (i < size) {
- val item: Episode = event.episodes[i++]
- val pos = Episodes.indexOfItemWithId(episodes, item.id)
- if (pos >= 0) {
- episodes.removeAt(pos)
- vms.removeAt(pos)
- val media = item.media
- if (media != null && media.downloaded) {
- episodes.add(pos, item)
- vms.add(pos, EpisodeVM(item))
- }
- }
- }
-// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash
-// if (size > 0) adapter.updateItems(episodes)
- refreshInfoBar()
- }
-
- private fun onEpisodeMediaEvent(event: FlowEvent.EpisodeMediaEvent) {
-// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
- var i = 0
- val size: Int = event.episodes.size
- while (i < size) {
- val item: Episode = event.episodes[i++]
- val pos = Episodes.indexOfItemWithId(episodes, item.id)
- if (pos >= 0) {
- episodes.removeAt(pos)
- vms.removeAt(pos)
- val media = item.media
- if (media != null && media.downloaded) {
- episodes.add(pos, item)
- vms.add(pos, EpisodeVM(item))
- }
- }
- }
-// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash
-// if (size > 0) adapter.updateItems(episodes)
- refreshInfoBar()
- }
-
- private fun refreshSwipeTelltale() {
- leftActionState.value = swipeActions.actions.left[0]
- rightActionState.value = swipeActions.actions.right[0]
- }
-
- private var loadItemsRunning = false
- private fun loadItems() {
-// emptyView.hide()
- Logd(TAG, "loadItems() called")
- if (!loadItemsRunning) {
- loadItemsRunning = true
- lifecycleScope.launch {
- try {
- withContext(Dispatchers.IO) {
- val sortOrder: EpisodeSortOrder? = downloadsSortedOrder
- val filter = getFilter()
- val downloadedItems = getEpisodes(0, Int.MAX_VALUE, filter, sortOrder, false)
- if (runningDownloads.isEmpty()) {
- episodes.clear()
- episodes.addAll(downloadedItems)
- } else {
- val mediaUrls: MutableList = ArrayList()
- for (url in runningDownloads) {
- if (Episodes.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
- mediaUrls.add(url)
- }
- val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList()
- currentDownloads.addAll(downloadedItems)
- episodes.clear()
- episodes.addAll(currentDownloads)
- }
-// episodes.retainAll { filter.matchesForQueues(it) }
- withContext(Dispatchers.Main) {
- vms.clear()
- for (e in episodes) vms.add(EpisodeVM(e))
- }
- }
- withContext(Dispatchers.Main) { refreshInfoBar() }
- } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
- } finally { loadItemsRunning = false }
- }
- }
- }
-
- private fun getEpisdesWithUrl(urls: List): List {
- Logd(TAG, "getEpisdesWithUrl() called ")
- if (urls.isEmpty()) return listOf()
- val episodes_: MutableList = mutableListOf()
- for (url in urls) {
- val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue
- val item_ = media.episodeOrFetch()
- if (item_ != null) episodes_.add(item_)
- }
- return realm.copyFromRealm(episodes_)
- }
-
- private fun refreshInfoBar() {
- var info = String.format(Locale.getDefault(), "%d%s", episodes.size, getString(R.string.episodes_suffix))
- if (episodes.isNotEmpty()) {
- var sizeMB: Long = 0
- for (item in episodes) sizeMB += item.media?.size ?: 0
- info += " • " + (sizeMB / 1000000) + " MB"
- }
- Logd(TAG, "refreshInfoBar filter value: ${getFilter().properties.size} ${getFilter().properties.joinToString()}")
- if (getFilter().properties.size > 1) info += " - ${getString(R.string.filtered_label)}"
- infoBarText.value = info
- }
-
- companion object {
- val TAG = DownloadsFragment::class.simpleName ?: "Anonymous"
-
- const val ARG_SHOW_LOGS: String = "show_logs"
- private const val KEY_UP_ARROW = "up_arrow"
-
- // the sort order for the downloads.
- var downloadsSortedOrder: EpisodeSortOrder?
- get() {
- val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
- return EpisodeSortOrder.fromCodeString(sortOrderStr)
- }
- set(sortOrder) {
- appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + sortOrder!!.code).apply()
- }
-
- var prefFilterDownloads: String
- get() = appPrefs.getString(UserPreferences.Prefs.prefDownloadsFilter.name, EpisodeFilter.States.downloaded.name) ?: EpisodeFilter.States.downloaded.name
- set(filter) {
- appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadsFilter.name, filter).apply()
- }
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt
new file mode 100644
index 00000000..84adf7ea
--- /dev/null
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt
@@ -0,0 +1,354 @@
+package ac.mdiq.podcini.ui.fragment
+
+import ac.mdiq.podcini.R
+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
+import ac.mdiq.podcini.storage.database.RealmDB.upsert
+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.EpisodeMedia
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder
+import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
+import ac.mdiq.podcini.ui.actions.DeleteActionButton
+import ac.mdiq.podcini.ui.compose.CustomTheme
+import ac.mdiq.podcini.ui.compose.EpisodeVM
+import ac.mdiq.podcini.ui.compose.SpinnerExternalSet
+import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
+import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
+import ac.mdiq.podcini.util.EventFlow
+import ac.mdiq.podcini.util.FlowEvent
+import ac.mdiq.podcini.util.Logd
+import android.content.Context
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ComposeView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.withContext
+import org.apache.commons.lang3.StringUtils
+import java.io.File
+import java.util.*
+import kotlin.math.min
+
+class EpisodesFragment : BaseEpisodesFragment() {
+ val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) }
+
+ private val spinnerTexts = QuickAccess.entries.map { it.name }
+ private var curIndex by mutableIntStateOf(0)
+ private lateinit var spinnerView: ComposeView
+
+ private var startDate : Long = 0L
+ private var endDate : Long = Date().time
+
+ private var episodesSortOrder: EpisodeSortOrder
+ get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(UserPreferences.Prefs.prefEpisodesSort.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
+ set(s) {
+ appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesSort.name, "" + s.code).apply()
+ }
+ private var prefFilterEpisodes: String
+ get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodesFilter.name, "")?:""
+ set(filter) {
+ appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesFilter.name, filter).apply()
+ }
+ private var prefFilterDownloads: String
+ get() = appPrefs.getString(UserPreferences.Prefs.prefDownloadsFilter.name, EpisodeFilter.States.downloaded.name) ?: EpisodeFilter.States.downloaded.name
+ set(filter) {
+ appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadsFilter.name, filter).apply()
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ val root = super.onCreateView(inflater, container, savedInstanceState)
+ Logd(TAG, "fragment onCreateView")
+
+ curIndex = prefs.getInt("curIndex", 0)
+ spinnerView = ComposeView(requireContext()).apply {
+ setContent {
+ CustomTheme(requireContext()) {
+ SpinnerExternalSet(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
+ Logd(QueuesFragment.Companion.TAG, "Item selected: $index")
+ curIndex = index
+ prefs.edit().putInt("curIndex", index).apply()
+ actionButtonToPass = if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name) {it -> DeleteActionButton(it) } else null
+ loadItems()
+ }
+ }
+ }
+ }
+ toolbar.addView(spinnerView)
+
+ toolbar.inflateMenu(R.menu.episodes)
+ sortOrder = episodesSortOrder
+ updateToolbar()
+ return root
+ }
+
+ /**
+ * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
+ * has been played ot completed at least once.
+ * @param limit The maximum number of episodes to return.
+ * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
+ */
+ fun getHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time,
+ sortOrder: EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD): List {
+ Logd(TAG, "getHistory() called")
+ val medias = realm.query(EpisodeMedia::class).query("(playbackCompletionTime > 0) OR (lastPlayedTime > \$0 AND lastPlayedTime <= \$1)", start, end).find()
+ var episodes: MutableList = mutableListOf()
+ for (m in medias) {
+ val item_ = m.episodeOrFetch()
+ if (item_ != null) episodes.add(item_)
+ else Logd(TAG, "getHistory: media has null episode: ${m.id}")
+ }
+ getPermutor(sortOrder).reorder(episodes)
+ if (offset > 0 && episodes.size > offset) episodes = episodes.subList(offset, min(episodes.size, offset+limit))
+ return episodes
+ }
+
+ override fun loadData(): List {
+ return when (spinnerTexts[curIndex]) {
+ QuickAccess.New.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), episodesSortOrder, false)
+ QuickAccess.Planned.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.soon.name, EpisodeFilter.States.later.name), episodesSortOrder, false)
+ QuickAccess.Repeats.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.again.name, EpisodeFilter.States.forever.name), episodesSortOrder, false)
+ QuickAccess.Liked.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.good.name, EpisodeFilter.States.superb.name), episodesSortOrder, false)
+ QuickAccess.Commented.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.has_comments.name), episodesSortOrder, false)
+ QuickAccess.History.name -> getHistory(0, Int.MAX_VALUE, sortOrder = episodesSortOrder).toMutableList()
+ QuickAccess.Downloaded.name -> getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(prefFilterDownloads), episodesSortOrder, false)
+ QuickAccess.All.name -> getEpisodes(0, Int.MAX_VALUE, getFilter(), episodesSortOrder, false)
+ else -> getEpisodes(0, Int.MAX_VALUE, getFilter(), episodesSortOrder, false)
+ }
+ }
+
+ override fun getFilter(): EpisodeFilter {
+ return EpisodeFilter(prefFilterEpisodes)
+ }
+
+ override fun getPrefName(): String {
+ return PREF_NAME
+ }
+
+ var progressing by mutableStateOf(false)
+ override fun updateToolbar() {
+ toolbar.menu.findItem(R.id.clear_new).isVisible = episodes.isNotEmpty() && spinnerTexts[curIndex] == QuickAccess.New.name
+ toolbar.menu.findItem(R.id.filter_items).isVisible = episodes.isNotEmpty() && spinnerTexts[curIndex] == QuickAccess.All.name
+ toolbar.menu.findItem(R.id.clear_history_item).isVisible = episodes.isNotEmpty() && spinnerTexts[curIndex] == QuickAccess.History.name
+ toolbar.menu.findItem(R.id.reconcile).isVisible = episodes.isNotEmpty() && spinnerTexts[curIndex] == QuickAccess.Downloaded.name
+
+ var info = "${episodes.size} episodes"
+ if (spinnerTexts[curIndex] == QuickAccess.All.name && getFilter().properties.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
+ else if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name && episodes.isNotEmpty()) {
+ var sizeMB: Long = 0
+ for (item in episodes) sizeMB += item.media?.size ?: 0
+ info += " • " + (sizeMB / 1000000) + " MB"
+ }
+ if (progressing) info += " - ${getString(R.string.progressing_label)}"
+ infoBarText.value = info
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ if (super.onMenuItemClick(item)) return true
+
+ when (item.itemId) {
+ R.id.filter_items -> {
+ if (spinnerTexts[curIndex] == QuickAccess.History.name) {
+ val dialog = object: DatesFilterDialog(requireContext(), 0L) {
+ override fun initParams() {
+ val calendar = Calendar.getInstance()
+ calendar.add(Calendar.YEAR, -1) // subtract 1 year
+ timeFilterFrom = calendar.timeInMillis
+ showMarkPlayed = false
+ }
+ override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) {
+ EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder, timeFilterFrom, timeFilterTo))
+ }
+ }
+ dialog.show()
+ } else showFilterDialog = true
+ }
+ R.id.episodes_sort -> showSortDialog = true
+ R.id.clear_history_item -> {
+ val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(), R.string.clear_history_label, R.string.clear_playback_history_msg) {
+ override fun onConfirmButtonPressed(dialog: DialogInterface) {
+ dialog.dismiss()
+ clearHistory()
+ }
+ }
+ conDialog.createNewDialog().show()
+ }
+ R.id.reconcile -> reconcile()
+ R.id.clear_new -> clearNew()
+ else -> return false
+ }
+ return true
+ }
+
+ private fun clearNew() {
+ runOnIOScope {
+ progressing = true
+ for (e in episodes) if (e.isNew) upsert(e) { it.setPlayed(false) }
+ withContext(Dispatchers.Main) {
+ progressing = false
+ Toast.makeText(requireContext(), "History cleared", Toast.LENGTH_LONG).show()
+ }
+ loadItems()
+ }
+ }
+
+ private val nameEpisodeMap: MutableMap = mutableMapOf()
+ private val filesRemoved: MutableList = mutableListOf()
+ private fun reconcile() {
+ fun traverse(srcFile: File, srcRootDir: File) {
+ val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1)
+ if (srcFile.isDirectory) {
+ Logd(TAG, "traverse folder title: $relativePath")
+ val dirFiles = srcFile.listFiles()
+ dirFiles?.forEach { file -> traverse(file, srcFile) }
+ } else {
+ Logd(TAG, "traverse: $srcFile")
+ val episode = nameEpisodeMap.remove(relativePath)
+ if (episode == null) {
+ Logd(TAG, "traverse: error: episode not exist in map: $relativePath")
+ filesRemoved.add(relativePath)
+ srcFile.delete()
+ return
+ }
+ Logd(TAG, "traverse found episode: ${episode.title}")
+ }
+ }
+ runOnIOScope {
+ progressing = true
+ val items = realm.query(Episode::class).query("media.episode == nil").find()
+ Logd(TAG, "number of episode with null backlink: ${items.size}")
+ for (item in items) if (item.media != null) upsert(item) { it.media!!.episode = it }
+ nameEpisodeMap.clear()
+ for (e in episodes) {
+ var fileUrl = e.media?.fileUrl ?: continue
+ fileUrl = fileUrl.substring(fileUrl.lastIndexOf('/') + 1)
+ Logd(TAG, "reconcile: fileUrl: $fileUrl")
+ nameEpisodeMap[fileUrl] = e
+ }
+ val mediaDir = requireContext().getExternalFilesDir("media") ?: return@runOnIOScope
+ mediaDir.listFiles()?.forEach { file -> traverse(file, mediaDir) }
+ Logd(TAG, "reconcile: end, episodes missing file: ${nameEpisodeMap.size}")
+ if (nameEpisodeMap.isNotEmpty()) for (e in nameEpisodeMap.values) upsertBlk(e) { it.media?.setfileUrlOrNull(null) }
+ loadItems()
+ Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}")
+ withContext(Dispatchers.Main) {
+ progressing = false
+ Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ fun clearHistory() : Job {
+ Logd(TAG, "clearHistory called")
+ return runOnIOScope {
+ progressing = true
+ val episodes = realm.query(Episode::class).query("media.playbackCompletionTime > 0 || media.lastPlayedTime > 0").find()
+ for (e in episodes) {
+ upsert(e) {
+ it.media?.playbackCompletionDate = null
+ it.media?.lastPlayedTime = 0
+ }
+ }
+ withContext(Dispatchers.Main) {
+ progressing = false
+ Toast.makeText(requireContext(), "History cleared", Toast.LENGTH_LONG).show()
+ }
+ EventFlow.postEvent(FlowEvent.HistoryEvent())
+ }
+ }
+
+ override fun onFilterChanged(filterValues: Set) {
+ if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name || spinnerTexts[curIndex] == QuickAccess.All.name) {
+ val fSet = filterValues.toMutableSet()
+ if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name) fSet.add(EpisodeFilter.States.downloaded.name)
+ prefFilterEpisodes = StringUtils.join(fSet, ",")
+ loadItems()
+ }
+ }
+
+ override fun onSort(order: EpisodeSortOrder) {
+ episodesSortOrder = order
+ loadItems()
+ }
+
+ override fun filtersDisabled(): MutableSet {
+ return if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name) mutableSetOf(EpisodeFilter.EpisodesFilterGroup.DOWNLOADED, EpisodeFilter.EpisodesFilterGroup.MEDIA)
+ else mutableSetOf()
+ }
+
+ override fun onHistoryEvent(event: FlowEvent.HistoryEvent) {
+ if (spinnerTexts[curIndex] == QuickAccess.History.name) {
+ sortOrder = event.sortOrder
+ if (event.startDate > 0) startDate = event.startDate
+ endDate = event.endDate
+ loadItems()
+ updateToolbar()
+ }
+ }
+
+ override fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
+ if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name) {
+ var i = 0
+ val size: Int = event.episodes.size
+ while (i < size) {
+ val item: Episode = event.episodes[i++]
+ val pos = Episodes.indexOfItemWithId(episodes, item.id)
+ if (pos >= 0) {
+ episodes.removeAt(pos)
+ vms.removeAt(pos)
+ val media = item.media
+ if (media != null && media.downloaded) {
+ episodes.add(pos, item)
+ vms.add(pos, EpisodeVM(item))
+ }
+ }
+ }
+ updateToolbar()
+ }
+ }
+
+ override fun onEpisodeMediaEvent(event: FlowEvent.EpisodeMediaEvent) {
+ if (spinnerTexts[curIndex] == QuickAccess.Downloaded.name) {
+ var i = 0
+ val size: Int = event.episodes.size
+ while (i < size) {
+ val item: Episode = event.episodes[i++]
+ val pos = Episodes.indexOfItemWithId(episodes, item.id)
+ if (pos >= 0) {
+ episodes.removeAt(pos)
+ vms.removeAt(pos)
+ val media = item.media
+ if (media != null && media.downloaded) {
+ episodes.add(pos, item)
+ vms.add(pos, EpisodeVM(item))
+ }
+ }
+ }
+ updateToolbar()
+ }
+ }
+
+ enum class QuickAccess {
+ New, Planned, Repeats, Liked, Commented, Downloaded, History, All
+ }
+
+ companion object {
+ val TAG = EpisodesFragment::class.simpleName ?: "Anonymous"
+ const val PREF_NAME: String = "PrefEpisodesFragment"
+ }
+}
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 3b4ff928..253ce25b 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
@@ -204,12 +204,12 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
leftSwipeCB = {
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else leftActionState.value.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
+ else leftActionState.value.performAction(it, this@FeedEpisodesFragment)
},
rightSwipeCB = {
Logd(TAG, "rightActionState: ${rightActionState.value.getId()}")
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else rightActionState.value.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
+ else rightActionState.value.performAction(it, this@FeedEpisodesFragment)
},
)
}
@@ -635,7 +635,7 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
withContext(Dispatchers.Main) {
Logd(TAG, "loadItems subscribe called ${feed?.title}")
rating = feed?.rating ?: Rating.UNRATED.code
- swipeActions.setFilter(feed?.episodeFilter)
+// swipeActions.setFilter(feed?.episodeFilter)
refreshHeaderView()
// if (feed != null) {
// adapter.updateItems(episodes, feed)
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
deleted file mode 100644
index 50c102ac..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-package ac.mdiq.podcini.ui.fragment
-
-import ac.mdiq.podcini.R
-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.Episode
-import ac.mdiq.podcini.storage.model.EpisodeMedia
-import ac.mdiq.podcini.storage.model.EpisodeSortOrder
-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
-import ac.mdiq.podcini.util.FlowEvent
-import ac.mdiq.podcini.util.Logd
-import android.content.DialogInterface
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import java.util.*
-import kotlin.math.min
-
-class HistoryFragment : BaseEpisodesFragment() {
-// private var sortOrder : EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
- private var startDate : Long = 0L
- private var endDate : Long = Date().time
- private var allHistory: List = listOf()
-
- override fun getPrefName(): String {
- return TAG
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- val root = super.onCreateView(inflater, container, savedInstanceState)
- Logd(TAG, "fragment onCreateView")
- sortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
- toolbar.inflateMenu(R.menu.playback_history)
- toolbar.setTitle(R.string.playback_history_label)
- updateToolbar()
- return root
- }
-
- override fun onStart() {
- super.onStart()
- procFlowEvents()
- }
-
- override fun onStop() {
- super.onStop()
- cancelFlowEvents()
- }
-
- override fun onDestroyView() {
- allHistory = listOf()
- super.onDestroyView()
- }
-
- override fun onSort(order: EpisodeSortOrder) {
-// EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder))
- sortOrder = order
- loadItems()
- updateToolbar()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onOptionsItemSelected(item)) return true
- when (item.itemId) {
- R.id.episodes_sort -> showSortDialog = true
- R.id.filter_items -> {
- val dialog = object: DatesFilterDialog(requireContext(), 0L) {
- override fun initParams() {
- val calendar = Calendar.getInstance()
- calendar.add(Calendar.YEAR, -1) // subtract 1 year
- timeFilterFrom = calendar.timeInMillis
- showMarkPlayed = false
- }
- override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) {
- EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder, timeFilterFrom, timeFilterTo))
- }
- }
- dialog.show()
- }
- R.id.clear_history_item -> {
- val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(), R.string.clear_history_label, R.string.clear_playback_history_msg) {
- override fun onConfirmButtonPressed(dialog: DialogInterface) {
- dialog.dismiss()
- clearHistory()
- }
- }
- conDialog.createNewDialog().show()
- }
- else -> return false
- }
- return true
- }
-
- override fun updateToolbar() {
- // Not calling super, as we do not have a refresh button that could be updated
- toolbar.menu.findItem(R.id.episodes_sort).isVisible = episodes.isNotEmpty()
- toolbar.menu.findItem(R.id.filter_items).isVisible = episodes.isNotEmpty()
- toolbar.menu.findItem(R.id.clear_history_item).isVisible = episodes.isNotEmpty()
-
- swipeActions.setFilter(getFilter())
- var info = "${episodes.size} episodes"
- if (getFilter().properties.isNotEmpty()) {
- info += " - ${getString(R.string.filtered_label)}"
- }
- infoBarText.value = info
- }
-
- private var eventSink: Job? = null
- private fun cancelFlowEvents() {
- eventSink?.cancel()
- eventSink = null
- }
- private fun procFlowEvents() {
- if (eventSink != null) return
- eventSink = lifecycleScope.launch {
- EventFlow.events.collectLatest { event ->
- Logd(TAG, "Received event: ${event.TAG}")
- when (event) {
- is FlowEvent.HistoryEvent -> {
- sortOrder = event.sortOrder
- if (event.startDate > 0) startDate = event.startDate
- endDate = event.endDate
- loadItems()
- updateToolbar()
- }
- else -> {}
- }
- }
- }
- }
-
- private var loadItemsRunning = false
- override fun loadData(): List {
- if (!loadItemsRunning) {
- loadItemsRunning = true
- allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList()
- loadItemsRunning = false
- }
- if (allHistory.isEmpty()) return listOf()
- return allHistory
- }
-
- override fun loadTotalItemCount(): Int {
- return getNumberOfPlayed().toInt()
- }
-
- fun clearHistory() : Job {
- Logd(TAG, "clearHistory called")
- return runOnIOScope {
- val episodes = realm.query(Episode::class).query("media.playbackCompletionTime > 0 || media.lastPlayedTime > 0").find()
- for (e in episodes) {
- upsert(e) {
- it.media?.playbackCompletionDate = null
- it.media?.lastPlayedTime = 0
- }
- }
- EventFlow.postEvent(FlowEvent.HistoryEvent())
- }
- }
-
- companion object {
- val TAG = HistoryFragment::class.simpleName ?: "Anonymous"
-
- fun getNumberOfPlayed(): Long {
- Logd(TAG, "getNumberOfPlayed called")
- return realm.query(EpisodeMedia::class).query("lastPlayedTime > 0 || playbackCompletionTime > 0").count().find()
- }
-
- /**
- * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
- * has been played ot completed at least once.
- * @param limit The maximum number of episodes to return.
- * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
- */
- fun getHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time,
- sortOrder: EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD): List {
- Logd(TAG, "getHistory() called")
- val medias = realm.query(EpisodeMedia::class).query("(playbackCompletionTime > 0) OR (lastPlayedTime > \$0 AND lastPlayedTime <= \$1)", start, end).find()
- var episodes: MutableList = mutableListOf()
- for (m in medias) {
- val item_ = m.episodeOrFetch()
- if (item_ != null) episodes.add(item_)
- else Logd(TAG, "getHistory: media has null episode: ${m.id}")
- }
- getPermutor(sortOrder).reorder(episodes)
- if (offset > 0 && episodes.size > offset) episodes = episodes.subList(offset, min(episodes.size, offset+limit))
- return episodes
- }
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt
index e3865d3e..17a3d044 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt
@@ -13,7 +13,7 @@ import ac.mdiq.podcini.storage.model.Rating.Companion.fromCode
import ac.mdiq.podcini.ui.actions.DownloadActionButton
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.ShareReceiverActivity.Companion.receiveShared
-import ac.mdiq.podcini.ui.compose.ConfirmAddYoutubeEpisode
+import ac.mdiq.podcini.ui.compose.ConfirmAddYoutubeEpisode1
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@@ -120,7 +120,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
var showYTMediaConfirmDialog by remember { mutableStateOf(false) }
var sharedUrl by remember { mutableStateOf("") }
if (showYTMediaConfirmDialog)
- ConfirmAddYoutubeEpisode(listOf(sharedUrl), showYTMediaConfirmDialog, onDismissRequest = { showYTMediaConfirmDialog = false })
+ ConfirmAddYoutubeEpisode1(listOf(sharedUrl), showYTMediaConfirmDialog, onDismissRequest = { showYTMediaConfirmDialog = false })
LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 6.dp, top = 5.dp, bottom = 5.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)) {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
index ab2fa432..89c6e231 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
@@ -1,6 +1,7 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
+import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.database.Feeds.getFeedCount
@@ -11,7 +12,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ARGUMENT_FEED_ID
-import ac.mdiq.podcini.ui.fragment.HistoryFragment.Companion.getNumberOfPlayed
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
@@ -98,8 +98,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
private fun getRecentPodcasts() {
var feeds_ = realm.query(Feed::class).sort("lastPlayed", sortOrder = Sort.DESCENDING).find().toMutableList()
- if (feeds_.size > 3) feeds_ = feeds_.subList(0, 3)
-// for (f in feeds_) Logd(TAG, "getRecentPodcasts ${f.title}")
+ if (feeds_.size > 5) feeds_ = feeds_.subList(0, 5)
feeds.clear()
feeds.addAll(feeds_)
}
@@ -114,6 +113,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
}) {
Icon(imageVector = ImageVector.vectorResource(nav.iconRes), tint = textColor, contentDescription = nav.tag, modifier = Modifier.padding(start = 10.dp))
+// val nametag = if (nav.tag != QueuesFragment.TAG) stringResource(nav.nameRes) else curQueue.name
Text(stringResource(nav.nameRes), color = textColor, style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(start = 20.dp))
Spacer(Modifier.weight(1f))
if (nav.count > 0) Text(nav.count.toString(), color = textColor, modifier = Modifier.padding(end = 10.dp))
@@ -219,11 +219,11 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
val navMap: LinkedHashMap = linkedMapOf(
SubscriptionsFragment.TAG to NavItem(SubscriptionsFragment.TAG, R.drawable.ic_subscriptions, R.string.subscriptions_label),
QueuesFragment.TAG to NavItem(QueuesFragment.TAG, R.drawable.ic_playlist_play, R.string.queue_label),
- AllEpisodesFragment.TAG to NavItem(AllEpisodesFragment.TAG, R.drawable.ic_feed, R.string.episodes_label),
- DownloadsFragment.TAG to NavItem(DownloadsFragment.TAG, R.drawable.ic_download, R.string.downloads_label),
- HistoryFragment.TAG to NavItem(HistoryFragment.TAG, R.drawable.baseline_work_history_24, R.string.playback_history_label),
+ EpisodesFragment.TAG to NavItem(EpisodesFragment.TAG, R.drawable.ic_feed, R.string.episodes_label),
+// AllEpisodesFragment.TAG to NavItem(AllEpisodesFragment.TAG, R.drawable.ic_feed, R.string.episodes_label),
+// DownloadsFragment.TAG to NavItem(DownloadsFragment.TAG, R.drawable.ic_download, R.string.downloads_label),
+// HistoryFragment.TAG to NavItem(HistoryFragment.TAG, R.drawable.baseline_work_history_24, R.string.playback_history_label),
LogsFragment.TAG to NavItem(LogsFragment.TAG, R.drawable.ic_history, R.string.logs_label),
-// SubscriptionLogFragment.TAG to NavItem(SubscriptionLogFragment.TAG, R.drawable.ic_subscriptions, R.string.subscriptions_log_label),
StatisticsFragment.TAG to NavItem(StatisticsFragment.TAG, R.drawable.ic_chart_box, R.string.statistics_label),
OnlineSearchFragment.TAG to NavItem(OnlineSearchFragment.TAG, R.drawable.ic_add, R.string.add_feed_label)
)
@@ -251,9 +251,10 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
feedCount = getFeedCount()
navMap[QueuesFragment.TAG]?.count = realm.query(PlayQueue::class).find().sumOf { it.size()}
navMap[SubscriptionsFragment.TAG]?.count = feedCount
- navMap[HistoryFragment.TAG]?.count = getNumberOfPlayed().toInt()
- navMap[DownloadsFragment.TAG]?.count = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
- navMap[AllEpisodesFragment.TAG]?.count = numItems
+// navMap[HistoryFragment.TAG]?.count = getNumberOfPlayed().toInt()
+// navMap[DownloadsFragment.TAG]?.count = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
+// navMap[AllEpisodesFragment.TAG]?.count = numItems
+ navMap[EpisodesFragment.TAG]?.count = numItems
navMap[LogsFragment.TAG]?.count = realm.query(ShareLog::class).count().find().toInt() +
realm.query(SubscriptionLog::class).count().find().toInt() +
realm.query(DownloadResult::class).count().find().toInt()
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
index d1ab0d1e..84869c24 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
@@ -709,7 +709,7 @@ class OnlineFeedFragment : Fragment() {
}
class RemoteEpisodesFragment : BaseEpisodesFragment() {
- private val episodeList: MutableList = mutableListOf()
+ private var episodeList: MutableList = mutableListOf()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
@@ -720,29 +720,21 @@ class OnlineFeedFragment : Fragment() {
return root
}
override fun onDestroyView() {
- episodeList.clear()
super.onDestroyView()
}
fun setEpisodes(episodeList_: MutableList) {
- episodeList.clear()
- episodeList.addAll(episodeList_)
+ episodeList = episodeList_
}
override fun loadData(): List {
- if (episodeList.isEmpty()) return listOf()
return episodeList
}
- override fun loadTotalItemCount(): Int {
- return episodeList.size
- }
override fun getPrefName(): String {
return PREF_NAME
}
override fun updateToolbar() {
- binding.toolbar.menu.findItem(R.id.episodes_sort).isVisible = false
-// binding.toolbar.menu.findItem(R.id.refresh_item).setVisible(false)
- binding.toolbar.menu.findItem(R.id.action_search).isVisible = false
-// binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false)
- binding.toolbar.menu.findItem(R.id.filter_items).isVisible = false
+ toolbar.menu.findItem(R.id.episodes_sort).isVisible = false
+ toolbar.menu.findItem(R.id.action_search).isVisible = false
+ toolbar.menu.findItem(R.id.filter_items).isVisible = false
infoBarText.value = "${episodes.size} episodes"
}
override fun onMenuItemClick(item: MenuItem): Boolean {
@@ -772,11 +764,6 @@ class OnlineFeedFragment : Fragment() {
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
private const val KEY_UP_ARROW = "up_arrow"
-// var prefs: SharedPreferences? = null
-// fun getSharedPrefs(context: Context) {
-// if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
-// }
-
fun newInstance(feedUrl: String, isShared: Boolean = false): OnlineFeedFragment {
val fragment = OnlineFeedFragment()
val b = Bundle()
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt
index a0123adf..17836250 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt
@@ -10,15 +10,17 @@ import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.net.feed.searcher.*
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.isOPMLRestared
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.performRestore
+import ac.mdiq.podcini.preferences.OpmlTransporter
+import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlElement
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.ui.activity.MainActivity
-import ac.mdiq.podcini.ui.activity.OpmlImportActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.NonlazyGrid
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
+import ac.mdiq.podcini.ui.compose.OpmlImportSelectionDialog
import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.feedCount
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@@ -98,10 +100,46 @@ class OnlineSearchFragment : Fragment() {
private var numColumns by mutableIntStateOf(4)
private val searchResult = mutableStateListOf()
+ private var showOpmlImportSelectionDialog by mutableStateOf(false)
+ private val readElements = mutableStateListOf()
private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
- this.chooseOpmlImportPathResult(uri) }
+ if (uri == null) return@registerForActivityResult
+ OpmlTransporter.startImport(requireContext(), uri) { readElements.addAll(it) }
+ showOpmlImportSelectionDialog = true
+ }
- private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) }
+ private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { uri: Uri? ->
+ if (uri == null) return@registerForActivityResult
+ val scope = CoroutineScope(Dispatchers.Main)
+ scope.launch {
+ try {
+ val feed = withContext(Dispatchers.IO) {
+// addLocalFolder(uri)
+ requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
+ requireNotNull(documentFile) { "Unable to retrieve document tree" }
+ var title = documentFile.name
+ if (title == null) title = getString(R.string.local_folder)
+
+ val dirFeed = Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title)
+ dirFeed.episodes.clear()
+ dirFeed.sortOrder = EpisodeSortOrder.EPISODE_TITLE_A_Z
+ val fromDatabase: Feed? = updateFeed(requireContext(), dirFeed, false)
+ FeedUpdateManager.runOnce(requireContext(), fromDatabase)
+ fromDatabase
+ }
+ withContext(Dispatchers.Main) {
+ if (feed != null) {
+ val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
+ mainAct?.loadChildFragment(fragment)
+ }
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
+ }
+ }
+ }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
@@ -170,6 +208,7 @@ class OnlineSearchFragment : Fragment() {
Text(stringResource(R.string.search_fyyd_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }))
Text(stringResource(R.string.gpodnet_search_hint), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_podcastindex_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }))
+ if (showOpmlImportSelectionDialog) OpmlImportSelectionDialog(readElements) { showOpmlImportSelectionDialog = false }
Text(stringResource(R.string.opml_add_podcast_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = {
try { chooseOpmlImportPathLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) {
@@ -332,48 +371,6 @@ class OnlineSearchFragment : Fragment() {
super.onDestroyView()
}
- private fun chooseOpmlImportPathResult(uri: Uri?) {
- if (uri == null) return
-
- val intent = Intent(context, OpmlImportActivity::class.java)
- intent.setData(uri)
- startActivity(intent)
- }
-
- private fun addLocalFolderResult(uri: Uri?) {
- if (uri == null) return
- val scope = CoroutineScope(Dispatchers.Main)
- scope.launch {
- try {
- val feed = withContext(Dispatchers.IO) { addLocalFolder(uri) }
- withContext(Dispatchers.Main) {
- if (feed != null) {
- val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
- mainAct?.loadChildFragment(fragment)
- }
- }
- } catch (e: Throwable) {
- Log.e(TAG, Log.getStackTraceString(e))
- mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
- }
- }
- }
-
- private fun addLocalFolder(uri: Uri): Feed? {
- requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
- val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
- requireNotNull(documentFile) { "Unable to retrieve document tree" }
- var title = documentFile.name
- if (title == null) title = getString(R.string.local_folder)
-
- val dirFeed = Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title)
- dirFeed.episodes.clear()
- dirFeed.sortOrder = EpisodeSortOrder.EPISODE_TITLE_A_Z
- val fromDatabase: Feed? = updateFeed(requireContext(), dirFeed, false)
- FeedUpdateManager.runOnce(requireContext(), fromDatabase)
- return fromDatabase
- }
-
private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() {
override fun createIntent(context: Context, input: Uri?): Intent {
return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
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 1aa6ce0d..6a543feb 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
@@ -168,9 +168,9 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
toolbar.addView(spinnerView)
swipeActions = SwipeActions(this, TAG)
- swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
+// swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
swipeActionsBin = SwipeActions(this, "$TAG.Bin")
- swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
+// swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
binding.mainView.setContent {
CustomTheme(requireContext()) {
@@ -179,11 +179,11 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
InforBar(infoBarText, leftAction = leftActionStateBin, rightAction = rightActionStateBin, actionConfig = { swipeActionsBin.showDialog() })
val leftCB = { episode: Episode ->
if (leftActionStateBin.value is NoActionSwipeAction) swipeActionsBin.showDialog()
- else leftActionStateBin.value.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
+ else leftActionStateBin.value.performAction(episode, this@QueuesFragment)
}
val rightCB = { episode: Episode ->
if (rightActionStateBin.value is NoActionSwipeAction) swipeActionsBin.showDialog()
- else rightActionStateBin.value.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
+ else rightActionStateBin.value.performAction(episode, this@QueuesFragment)
}
EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
}
@@ -200,11 +200,11 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
val leftCB = { episode: Episode ->
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else leftActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
+ else leftActionState.value.performAction(episode, this@QueuesFragment)
}
val rightCB = { episode: Episode ->
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else rightActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
+ else rightActionState.value.performAction(episode, this@QueuesFragment)
}
EpisodeLazyColumn(activity as MainActivity, vms = vms,
isDraggable = dragDropEnabled, dragCB = { iFrom, iTo -> runOnIOScope { moveInQueueSync(iFrom, iTo, true) } },
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 6f64e705..2eaddbb0 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
@@ -7,7 +7,6 @@ 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.ui.actions.SwipeAction
@@ -117,11 +116,11 @@ class SearchFragment : Fragment() {
EpisodeLazyColumn(activity as MainActivity, vms = vms,
leftSwipeCB = {
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else leftActionState.value.performAction(it, this@SearchFragment, swipeActions.filter ?: EpisodeFilter())
+ else leftActionState.value.performAction(it, this@SearchFragment)
},
rightSwipeCB = {
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
- else rightActionState.value.performAction(it, this@SearchFragment, swipeActions.filter ?: EpisodeFilter())
+ else rightActionState.value.performAction(it, this@SearchFragment)
},
)
}
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 4f1d9c85..557bf8df 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
@@ -2,7 +2,6 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
-import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.preferences.DocumentFileExportWorker
import ac.mdiq.podcini.preferences.ExportTypes
@@ -28,7 +27,10 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import android.app.Activity.RESULT_OK
-import android.content.*
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.util.Log
@@ -75,7 +77,6 @@ import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
@@ -532,9 +533,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (showSpeedDialog) PlaybackSpeedDialog(selected, initSpeed = 1f, maxSpeed = 3f, onDismiss = {showSpeedDialog = false}) { newSpeed ->
saveFeedPreferences { it: FeedPreferences -> it.playSpeed = newSpeed }
}
+ var showAutoDownloadSwitchDialog by remember { mutableStateOf(false) }
+ if (showAutoDownloadSwitchDialog) SimpleSwitchDialog(stringResource(R.string.auto_download_settings_label), stringResource(R.string.auto_download_label), onDismissRequest = { showAutoDownloadSwitchDialog = false }) { enabled ->
+ saveFeedPreferences { it: FeedPreferences -> it.autoDownload = enabled }
+ }
@Composable
- fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList, modifier: Modifier = Modifier) {
+ fun EpisodeSpeedDial(selected: SnapshotStateList, modifier: Modifier = Modifier) {
val TAG = "EpisodeSpeedDial ${selected.size}"
var isExpanded by remember { mutableStateOf(false) }
val options = listOf<@Composable () -> Unit>(
@@ -558,13 +563,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
isExpanded = false
selectMode = false
Logd(TAG, "ic_download: ${selected.size}")
- val preferenceSwitchDialog = PreferenceSwitchDialog(activity, activity.getString(R.string.auto_download_settings_label), activity.getString(R.string.auto_download_label))
- preferenceSwitchDialog.setOnPreferenceChangedListener( object: PreferenceSwitchDialog.OnPreferenceChangedListener {
- override fun preferenceChanged(enabled: Boolean) {
- saveFeedPreferences { it: FeedPreferences -> it.autoDownload = enabled }
- }
- })
- preferenceSwitchDialog.openDialog()
+ showAutoDownloadSwitchDialog = true
}) {
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
Text(stringResource(id = R.string.auto_download_label)) } },
@@ -834,7 +833,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "selectedIds: ${selected.size}")
}))
}
- EpisodeSpeedDial(activity as MainActivity, selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
+ EpisodeSpeedDial(selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
}
}
}
@@ -1430,33 +1429,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 {
- fun preferenceChanged(enabled: Boolean)
- }
- fun openDialog() {
- val builder = MaterialAlertDialogBuilder(context)
- builder.setTitle(title)
-
- val inflater = LayoutInflater.from(this.context)
- val layout = inflater.inflate(R.layout.dialog_switch_preference, null, false)
- val binding = DialogSwitchPreferenceBinding.bind(layout)
- val switchButton = binding.dialogSwitch
- switchButton.text = text
- builder.setView(layout)
-
- builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
- onPreferenceChangedListener?.preferenceChanged(switchButton.isChecked)
- }
- builder.setNegativeButton(R.string.cancel_label, null)
- builder.create().show()
- }
- fun setOnPreferenceChangedListener(onPreferenceChangedListener: OnPreferenceChangedListener?) {
- this.onPreferenceChangedListener = onPreferenceChangedListener
- }
- }
-
companion object {
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous"
diff --git a/app/src/main/res/layout/dialog_switch_preference.xml b/app/src/main/res/layout/dialog_switch_preference.xml
deleted file mode 100644
index 49882cad..00000000
--- a/app/src/main/res/layout/dialog_switch_preference.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/opml_selection.xml b/app/src/main/res/layout/opml_selection.xml
deleted file mode 100644
index 735c7f87..00000000
--- a/app/src/main/res/layout/opml_selection.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/episodes.xml b/app/src/main/res/menu/episodes.xml
index 578b57c1..19935c79 100644
--- a/app/src/main/res/menu/episodes.xml
+++ b/app/src/main/res/menu/episodes.xml
@@ -6,8 +6,14 @@
+ android:title="@string/search_label"
+ custom:showAsAction="always"/>
+
+
+ android:id="@+id/clear_history_item"
+ android:icon="@drawable/ic_delete"
+ android:title="@string/clear_history_label"
+ custom:showAsAction="never"/>
-
-
-
-
-
+
-
-
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 772f37bc..7a7dda45 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -127,6 +127,7 @@
New synthetic Youtube
Toggle grid list
Refreshing
+ Clear new
Reconcile
Chapters
@@ -213,6 +214,7 @@
Filtered
+ Processing
Open podcast
Open
@@ -441,6 +443,8 @@
backup, restore
Appearance
External elements
+ Create YT syndicates
+ Add to feed
Interruptions
Playback control
Reassign hardware buttons
diff --git a/changelog.md b/changelog.md
index 3c5a47c4..82fb4bd4 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,19 @@
+# 6.15.0
+
+* added a combo Epiosdes fragment with easy access to various filters
+ * merged AllEpisodes, History and Download into Episodes
+ * other easy accesses include: New, Planned (for Soon and Later), Repeats (for Again and Forever), Liked (for Good and Super)
+ * New episodes can be cleared via the manu
+* drawer items customization is disabled in Settings
+* drawer includes 5 most recent feeds
+* rearranged routines in ImportExportPreferences
+* importing OPML file is done within the activity, no longer starts OpmlImportActivity
+* removed OpmlImportActivity
+* getting shared youtube media has a new interface to select feed
+* cleaned up SwipeActions, removed the need for filter
+* more Compose migrations
+* media3 update to 1.5.0
+
# 6.14.8
* fixed issues in tags setting
diff --git a/fastlane/metadata/android/en-US/changelogs/3020308.txt b/fastlane/metadata/android/en-US/changelogs/3020308.txt
new file mode 100644
index 00000000..f2824b0a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3020308.txt
@@ -0,0 +1,15 @@
+ Version 6.15.0
+
+* added a combo Epiosdes fragment with easy access to various filters
+ * merged AllEpisodes, History and Download into Episodes
+ * other easy accesses include: New, Planned (for Soon and Later), Repeats (for Again and Forever), Liked (for Good and Super)
+ * New episodes can be cleared via the manu
+* drawer items customization is disabled in Settings
+* drawer includes 5 most recent feeds
+* rearranged routines in ImportExportPreferences
+* importing OPML file is done within the activity, no longer starts OpmlImportActivity
+* removed OpmlImportActivity
+* getting shared youtube media has a new interface to select feed
+* cleaned up SwipeActions, removed the need for filter
+* more Compose migrations
+* media3 update to 1.5.0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3bbe0d7e..d8c0f21b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,10 +30,10 @@ libraryBase = "2.1.0"
lifecycleRuntimeKtx = "2.8.7"
material3 = "1.3.1"
materialVersion = "1.12.0"
-media3Common = "1.4.1"
-media3Session = "1.4.1"
-media3Ui = "1.4.1"
-media3Exoplayer = "1.4.1"
+media3Common = "1.5.0"
+media3Session = "1.5.0"
+media3Ui = "1.5.0"
+media3Exoplayer = "1.5.0"
mediarouter = "1.7.0"
okhttp = "4.12.0"
okhttpUrlconnection = "4.12.0"