6.15.0 commit
This commit is contained in:
parent
8c13b7e5a1
commit
884abb5eec
|
@ -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 = ""
|
||||
|
|
|
@ -190,32 +190,6 @@
|
|||
android:resource="@xml/player_widget_info"/>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.OpmlImportActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/opml_import_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:mimeType="text/xml"/>
|
||||
<data android:mimeType="text/x-opml"/>
|
||||
<data android:mimeType="application/xml"/>
|
||||
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
||||
<!-- <data android:host="*"/>-->
|
||||
<data android:pathPattern="/.*\\.xml" />
|
||||
<data android:pathPattern="/.*\\.opml" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.activity.BugReportActivity"
|
||||
android:label="@string/bug_report_title">
|
||||
|
@ -257,6 +231,7 @@
|
|||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:host="*"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:mimeType="text/xml"/>
|
||||
<data android:mimeType="application/rss+xml"/>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Episode>()
|
||||
comItems.addAll(pausedItems)
|
||||
comItems.addAll(readItems)
|
||||
|
|
|
@ -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<OpmlElement>)->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()
|
||||
// }
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String>
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,13 +7,17 @@ import java.io.Serializable
|
|||
class EpisodeFilter(vararg properties_: String) : Serializable {
|
||||
val properties: HashSet<String> = 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<String> = mutableListOf()
|
||||
val mediaTypeQuerys = mutableListOf<String>()
|
||||
|
@ -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),
|
||||
|
|
|
@ -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<EpisodeSortOrder>().firstOrNull { it.code == code }
|
||||
fun fromCode(code: Int): EpisodeSortOrder {
|
||||
return enumValues<EpisodeSortOrder>().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<String?>): Array<EpisodeSortOrder?> {
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<String>? = null
|
||||
private var readElements: ArrayList<OpmlElement>? = null
|
||||
|
||||
private val titleList: List<String>
|
||||
get() {
|
||||
val result: MutableList<String> = 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"
|
||||
}
|
||||
}
|
|
@ -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<String, Uri>(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<String, Uri>(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
|
||||
private var showOpmlImportSelectionDialog by mutableStateOf(false)
|
||||
private val readElements = mutableStateListOf<OpmlElement>()
|
||||
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) {
|
||||
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(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<Intent>, 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<EpisodeAction> = 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<Episode>()
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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() })
|
||||
}
|
||||
}
|
|
@ -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<Episode>, 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<Feed?>(null) }
|
||||
if (synthetics.isNotEmpty()) {
|
||||
|
@ -455,7 +457,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
|
||||
val showConfirmYoutubeDialog = remember { mutableStateOf(false) }
|
||||
val youtubeUrls = remember { mutableListOf<String>() }
|
||||
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<EpisodeVM>, 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<EpisodeVM>, feed:
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
fun ConfirmAddYoutubeEpisode1(sharedUrls: List<String>, 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<Int, Boolean>() }
|
||||
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<Feed?>(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<String>, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<OpmlTransporter.OpmlElement>, onDismissRequest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val selectedItems = remember { mutableStateMapOf<Int, Boolean>() }
|
||||
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") } }
|
||||
)
|
||||
}
|
|
@ -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<Episode> = 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<Episode> {
|
||||
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<String>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<SwipeAction>(NoActionSwipeAction())
|
||||
private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
|
||||
|
@ -52,11 +50,13 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
lateinit var swipeActions: SwipeActions
|
||||
|
||||
val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
protected val vms = mutableStateListOf<EpisodeVM>()
|
||||
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<String>) {}
|
||||
|
||||
open fun filtersDisabled(): MutableSet<EpisodeFilter.EpisodesFilterGroup> {
|
||||
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<Episode>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> = HashSet()
|
||||
private val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var infoBarText = mutableStateOf("")
|
||||
private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
|
||||
private var rightActionState = mutableStateOf<SwipeAction>(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<String, Episode> = mutableMapOf()
|
||||
private val filesRemoved: MutableList<String> = 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<String> = 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<String> = 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<String>): List<Episode> {
|
||||
Logd(TAG, "getEpisdesWithUrl() called ")
|
||||
if (urls.isEmpty()) return listOf()
|
||||
val episodes_: MutableList<Episode> = 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Episode> {
|
||||
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<Episode> = 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<Episode> {
|
||||
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<String, Episode> = mutableMapOf()
|
||||
private val filesRemoved: MutableList<String> = 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<String>) {
|
||||
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<EpisodeFilter.EpisodesFilterGroup> {
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<Episode> = 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<Episode> {
|
||||
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<Episode> {
|
||||
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<Episode> = 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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<String, NavItem> = 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()
|
||||
|
|
|
@ -709,7 +709,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
}
|
||||
|
||||
class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
||||
private val episodeList: MutableList<Episode> = mutableListOf()
|
||||
private var episodeList: MutableList<Episode> = 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<Episode>) {
|
||||
episodeList.clear()
|
||||
episodeList.addAll(episodeList_)
|
||||
episodeList = episodeList_
|
||||
}
|
||||
override fun loadData(): List<Episode> {
|
||||
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()
|
||||
|
|
|
@ -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<PodcastSearchResult>()
|
||||
|
||||
private var showOpmlImportSelectionDialog by mutableStateOf(false)
|
||||
private val readElements = mutableStateListOf<OpmlElement>()
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(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<Uri?, Uri>(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) }
|
||||
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(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)
|
||||
|
|
|
@ -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) } },
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<Feed>, modifier: Modifier = Modifier) {
|
||||
fun EpisodeSpeedDial(selected: SnapshotStateList<Feed>, 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"
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/dialog_switch_preference"
|
||||
android:padding="24dp">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/dialogSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Switch" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/butConfirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/confirm_label"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/butCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toLeftOf="@+id/butConfirm"
|
||||
android:layout_toStartOf="@+id/butConfirm"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/cancel_label"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/feedlist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_above="@id/butConfirm"
|
||||
android:layout_alignParentTop="true"
|
||||
tools:listitem="@android:layout/simple_list_item_multiple_choice" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -6,8 +6,14 @@
|
|||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search"
|
||||
custom:showAsAction="always"
|
||||
android:title="@string/search_label"/>
|
||||
android:title="@string/search_label"
|
||||
custom:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/episodes_sort"
|
||||
android:icon="@drawable/arrows_sort"
|
||||
android:title="@string/sort"
|
||||
custom:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/filter_items"
|
||||
|
@ -17,19 +23,19 @@
|
|||
custom:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/episodes_sort"
|
||||
android:icon="@drawable/arrows_sort"
|
||||
android:title="@string/sort"
|
||||
custom:showAsAction="ifRoom" />
|
||||
android:id="@+id/clear_history_item"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/clear_history_label"
|
||||
custom:showAsAction="never"/>
|
||||
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/refresh_item"-->
|
||||
<!-- android:title="@string/refresh_label"-->
|
||||
<!-- android:menuCategory="container"-->
|
||||
<!-- custom:showAsAction="never" />-->
|
||||
<item
|
||||
android:id="@+id/reconcile"
|
||||
android:title="@string/reconcile_label"
|
||||
custom:showAsAction="never" />
|
||||
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/switch_queue"-->
|
||||
<!-- android:title="@string/switch_queue" />-->
|
||||
<item
|
||||
android:id="@+id/clear_new"
|
||||
android:title="@string/clear_new_label"
|
||||
custom:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
<string name="new_synth_yt_label">New synthetic Youtube</string>
|
||||
<string name="toggle_grid_list">Toggle grid list</string>
|
||||
<string name="refreshing_label">Refreshing</string>
|
||||
<string name="clear_new_label">Clear new</string>
|
||||
<string name="reconcile_label">Reconcile</string>
|
||||
<string name="chapters_label">Chapters</string>
|
||||
|
||||
|
@ -213,6 +214,7 @@
|
|||
|
||||
|
||||
<string name="filtered_label">Filtered</string>
|
||||
<string name="progressing_label">Processing</string>
|
||||
|
||||
<string name="open_podcast">Open podcast</string>
|
||||
<string name="open">Open</string>
|
||||
|
@ -441,6 +443,8 @@
|
|||
<string name="import_export_search_keywords">backup, restore</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="external_elements">External elements</string>
|
||||
<string name="create_YT_syndicates">Create YT syndicates</string>
|
||||
<string name="add_to_feed">Add to feed</string>
|
||||
<string name="interruptions">Interruptions</string>
|
||||
<string name="playback_control">Playback control</string>
|
||||
<string name="reassign_hardware_buttons">Reassign hardware buttons</string>
|
||||
|
|
16
changelog.md
16
changelog.md
|
@ -1,3 +1,19 @@
|
|||
# 6.15.0
|
||||
|
||||
* added a combo Epiosdes fragment with easy access to various filters
|
||||
* merged AllEpisodes, History and Download into Episodes
|
||||
* other easy accesses include: New, Planned (for Soon and Later), Repeats (for Again and Forever), Liked (for Good and Super)
|
||||
* New episodes can be cleared via the manu
|
||||
* drawer items customization is disabled in Settings
|
||||
* drawer includes 5 most recent feeds
|
||||
* rearranged routines in ImportExportPreferences
|
||||
* importing OPML file is done within the activity, no longer starts OpmlImportActivity
|
||||
* removed OpmlImportActivity
|
||||
* getting shared youtube media has a new interface to select feed
|
||||
* cleaned up SwipeActions, removed the need for filter
|
||||
* more Compose migrations
|
||||
* media3 update to 1.5.0
|
||||
|
||||
# 6.14.8
|
||||
|
||||
* fixed issues in tags setting
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
Version 6.15.0
|
||||
|
||||
* added a combo Epiosdes fragment with easy access to various filters
|
||||
* merged AllEpisodes, History and Download into Episodes
|
||||
* other easy accesses include: New, Planned (for Soon and Later), Repeats (for Again and Forever), Liked (for Good and Super)
|
||||
* New episodes can be cleared via the manu
|
||||
* drawer items customization is disabled in Settings
|
||||
* drawer includes 5 most recent feeds
|
||||
* rearranged routines in ImportExportPreferences
|
||||
* importing OPML file is done within the activity, no longer starts OpmlImportActivity
|
||||
* removed OpmlImportActivity
|
||||
* getting shared youtube media has a new interface to select feed
|
||||
* cleaned up SwipeActions, removed the need for filter
|
||||
* more Compose migrations
|
||||
* media3 update to 1.5.0
|
|
@ -30,10 +30,10 @@ libraryBase = "2.1.0"
|
|||
lifecycleRuntimeKtx = "2.8.7"
|
||||
material3 = "1.3.1"
|
||||
materialVersion = "1.12.0"
|
||||
media3Common = "1.4.1"
|
||||
media3Session = "1.4.1"
|
||||
media3Ui = "1.4.1"
|
||||
media3Exoplayer = "1.4.1"
|
||||
media3Common = "1.5.0"
|
||||
media3Session = "1.5.0"
|
||||
media3Ui = "1.5.0"
|
||||
media3Exoplayer = "1.5.0"
|
||||
mediarouter = "1.7.0"
|
||||
okhttp = "4.12.0"
|
||||
okhttpUrlconnection = "4.12.0"
|
||||
|
|
Loading…
Reference in New Issue