From 884abb5eec741303d89201c240551402747f2a95 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:28:02 +0100 Subject: [PATCH] 6.15.0 commit --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 27 +- .../service/DownloadServiceInterfaceImpl.kt | 24 +- .../podcini/net/sync/wifi/WifiSyncService.kt | 2 +- .../podcini/preferences/OpmlTransporter.kt | 79 +-- .../podcini/preferences/UserPreferences.kt | 6 +- .../ac/mdiq/podcini/storage/database/Feeds.kt | 31 +- .../mdiq/podcini/storage/database/Queues.kt | 6 +- .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../ac/mdiq/podcini/storage/model/Episode.kt | 5 + .../podcini/storage/model/EpisodeFilter.kt | 18 +- .../podcini/storage/model/EpisodeSortOrder.kt | 19 +- .../mdiq/podcini/ui/actions/SwipeActions.kt | 139 ++--- .../mdiq/podcini/ui/activity/MainActivity.kt | 14 +- .../podcini/ui/activity/OpmlImportActivity.kt | 248 -------- .../podcini/ui/activity/PreferenceActivity.kt | 530 ++++++++---------- .../ui/activity/ShareReceiverActivity.kt | 4 +- .../ac/mdiq/podcini/ui/compose/Composables.kt | 52 ++ .../ac/mdiq/podcini/ui/compose/EpisodesVM.kt | 65 ++- .../ac/mdiq/podcini/ui/compose/Feeds.kt | 65 ++- .../ui/fragment/AllEpisodesFragment.kt | 112 ---- .../ui/fragment/BaseEpisodesFragment.kt | 69 ++- .../podcini/ui/fragment/DownloadsFragment.kt | 426 -------------- .../podcini/ui/fragment/EpisodesFragment.kt | 354 ++++++++++++ .../ui/fragment/FeedEpisodesFragment.kt | 6 +- .../podcini/ui/fragment/HistoryFragment.kt | 199 ------- .../mdiq/podcini/ui/fragment/LogsFragment.kt | 4 +- .../podcini/ui/fragment/NavDrawerFragment.kt | 21 +- .../podcini/ui/fragment/OnlineFeedFragment.kt | 23 +- .../ui/fragment/OnlineSearchFragment.kt | 87 ++- .../podcini/ui/fragment/QueuesFragment.kt | 12 +- .../podcini/ui/fragment/SearchFragment.kt | 5 +- .../ui/fragment/SubscriptionsFragment.kt | 50 +- .../res/layout/dialog_switch_preference.xml | 16 - app/src/main/res/layout/opml_selection.xml | 44 -- app/src/main/res/menu/episodes.xml | 34 +- app/src/main/res/values/strings.xml | 4 + changelog.md | 16 + .../android/en-US/changelogs/3020308.txt | 15 + gradle/libs.versions.toml | 8 +- 40 files changed, 1082 insertions(+), 1763 deletions(-) delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt create mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodesFragment.kt delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt delete mode 100644 app/src/main/res/layout/dialog_switch_preference.xml delete mode 100644 app/src/main/res/layout/opml_selection.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020308.txt 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 @@ - - - -