6.9.0 commit
This commit is contained in:
parent
13a4f8c5b2
commit
72f28ce9b7
|
@ -16,6 +16,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
|||
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
|
||||
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
|
||||
#### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two.
|
||||
#### Since version 6.8.5, Podcini.R is built to target SDK 30 (Android 11), though built with SDK 35 and tested on Android 14. This is to counter 2-year old Google issue ForegroundServiceStartNotAllowedException. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
|
||||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||
|
||||
This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
||||
|
@ -138,6 +139,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
|||
* All the media from Youtube or Youtube Music can be played (only streamed) with video in fullscreen and in window modes or in audio only mode in the background
|
||||
* These media are played with the lowest video quality and highest audio quality
|
||||
* If a subscription is set for "audio only", then only audio stream is fetched at play time for every media in the subscription
|
||||
* accepted host names include: youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, and youtu.be
|
||||
|
||||
### Instant (or Wifi) sync
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020264
|
||||
versionName "6.8.7"
|
||||
versionCode 3020265
|
||||
versionName "6.9.0"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -25,10 +25,7 @@ import android.content.Context
|
|||
import android.util.Log
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
@ -50,43 +47,51 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val feed_ = Feed(selectedDownloadUrl, null)
|
||||
feed_.isBuilding = true
|
||||
feed_.id = Feed.newId()
|
||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||
feed_.hasVideoMedia = true
|
||||
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||
val eList: RealmList<Episode> = realmListOf()
|
||||
val eList: MutableList<Episode> = mutableListOf()
|
||||
|
||||
if (url.startsWith("https://youtube.com/playlist?") || url.startsWith("https://music.youtube.com/playlist?")) {
|
||||
val uURL = URL(url)
|
||||
// if (url.startsWith("https://youtube.com/playlist?") || url.startsWith("https://music.youtube.com/playlist?")) {
|
||||
if (uURL.path.startsWith("/playlist") || uURL.path.startsWith("/playlist")) {
|
||||
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch
|
||||
feed_.title = playlistInfo.name
|
||||
feed_.description = playlistInfo.description?.content ?: ""
|
||||
feed_.author = playlistInfo.uploaderName
|
||||
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||
feed_.episodes = realmListOf()
|
||||
var infoItems = playlistInfo.relatedItems
|
||||
var nextPage = playlistInfo.nextPage
|
||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || eList.size > 500) break
|
||||
try {
|
||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (infoItems.isNotEmpty()) {
|
||||
eList.clear()
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||
try {
|
||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
}
|
||||
feed_.isBuilding = false
|
||||
}
|
||||
feed_.episodes = eList
|
||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||
} else {
|
||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
||||
|
@ -102,32 +107,37 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
feed_.description = channelInfo.description
|
||||
feed_.author = channelInfo.parentChannelName
|
||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
feed_.episodes = realmListOf()
|
||||
|
||||
var infoItems = channelTabInfo.relatedItems
|
||||
var nextPage = channelTabInfo.nextPage
|
||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || eList.size > 200) break
|
||||
try {
|
||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (infoItems.isNotEmpty()) {
|
||||
eList.clear()
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||
try {
|
||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
}
|
||||
feed_.isBuilding = false
|
||||
}
|
||||
feed_.episodes = eList
|
||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "startFeedBuilding error1 ${e.message}")
|
||||
|
@ -202,8 +212,11 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
val destinationFile = File(destination)
|
||||
return try {
|
||||
val feed = Feed(selectedDownloadUrl, null)
|
||||
feed.isBuilding = true
|
||||
feed.fileUrl = destination
|
||||
FeedHandler().parseFeed(feed)
|
||||
val result = FeedHandler().parseFeed(feed)
|
||||
feed.isBuilding = false
|
||||
result
|
||||
} catch (e: FeedHandler.UnsupportedFeedtypeException) {
|
||||
Logd(TAG, "Unsupported feed type detected")
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
|
@ -250,6 +263,9 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
}
|
||||
|
||||
fun subscribe(feed: Feed) {
|
||||
while (feed.isBuilding) {
|
||||
runBlocking { delay(200) }
|
||||
}
|
||||
feed.id = 0L
|
||||
for (item in feed.episodes) {
|
||||
item.id = 0L
|
||||
|
|
|
@ -2,6 +2,9 @@ package ac.mdiq.podcini.net.feed.discovery
|
|||
|
||||
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetPodcast
|
||||
import ac.mdiq.vista.extractor.channel.ChannelInfoItem
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import de.mfietz.fyydlin.SearchHit
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
@ -18,6 +21,9 @@ class PodcastSearchResult private constructor(
|
|||
val subscriberCount: Int,
|
||||
val source: String) {
|
||||
|
||||
// feedId will be positive if already subscribed
|
||||
var feedId by mutableLongStateOf(0L)
|
||||
|
||||
companion object {
|
||||
fun dummy(): PodcastSearchResult {
|
||||
return PodcastSearchResult("", "", "", "", 0, "", -1, "dummy")
|
||||
|
|
|
@ -25,7 +25,6 @@ class FeedHandler {
|
|||
// val tg = TypeGetter()
|
||||
val type = getType(feed)
|
||||
val handler = SyndHandler(feed, type)
|
||||
|
||||
if (feed.fileUrl != null) {
|
||||
val factory = SAXParserFactory.newInstance()
|
||||
factory.isNamespaceAware = true
|
||||
|
|
|
@ -436,6 +436,7 @@ object Feeds {
|
|||
feed.preferences!!.queueId = -2L
|
||||
feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
||||
upsertBlk(feed) {}
|
||||
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
|
||||
return feed
|
||||
}
|
||||
|
||||
|
@ -452,6 +453,7 @@ object Feeds {
|
|||
upsertBlk(episode) {}
|
||||
feed.episodes.add(episode)
|
||||
upsertBlk(feed) {}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||
}
|
||||
|
||||
private fun getMiscSyndicate(): Feed {
|
||||
|
@ -470,6 +472,7 @@ object Feeds {
|
|||
feed.preferences!!.queueId = -2L
|
||||
// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
||||
upsertBlk(feed) {}
|
||||
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
|
||||
return feed
|
||||
}
|
||||
|
||||
|
@ -485,6 +488,7 @@ object Feeds {
|
|||
upsertBlk(episode) {}
|
||||
feed.episodes.add(episode)
|
||||
upsertBlk(feed) {}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -135,15 +135,9 @@ class Episode : RealmObject {
|
|||
return field
|
||||
}
|
||||
|
||||
@Ignore
|
||||
val downloadState = mutableIntStateOf(if (media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
||||
|
||||
@Ignore
|
||||
val isRemote = mutableStateOf(false)
|
||||
|
||||
@Ignore
|
||||
val stopMonitoring = mutableStateOf(false)
|
||||
|
||||
constructor() {
|
||||
this.playState = PlayState.UNPLAYED.code
|
||||
}
|
||||
|
@ -162,13 +156,6 @@ class Episode : RealmObject {
|
|||
this.feed = feed
|
||||
}
|
||||
|
||||
fun copyStates(other: Episode) {
|
||||
// inQueueState.value = other.inQueueState.value
|
||||
// isPlayingState.value = other.isPlayingState.value
|
||||
downloadState.value = other.downloadState.value
|
||||
stopMonitoring.value = other.stopMonitoring.value
|
||||
}
|
||||
|
||||
fun updateFromOther(other: Episode) {
|
||||
if (other.imageUrl != null) this.imageUrl = other.imageUrl
|
||||
if (other.title != null) title = other.title
|
||||
|
@ -296,10 +283,6 @@ class Episode : RealmObject {
|
|||
if (isFavorite != other.isFavorite) return false
|
||||
if (isInProgress != other.isInProgress) return false
|
||||
if (isDownloaded != other.isDownloaded) return false
|
||||
// if (inQueueState != other.inQueueState) return false
|
||||
// if (isPlayingState != other.isPlayingState) return false
|
||||
if (downloadState != other.downloadState) return false
|
||||
if (stopMonitoring != other.stopMonitoring) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -324,24 +307,9 @@ class Episode : RealmObject {
|
|||
result = 31 * result + isFavorite.hashCode()
|
||||
result = 31 * result + isInProgress.hashCode()
|
||||
result = 31 * result + isDownloaded.hashCode()
|
||||
// result = 31 * result + inQueueState.hashCode()
|
||||
// result = 31 * result + isPlayingState.hashCode()
|
||||
result = 31 * result + downloadState.hashCode()
|
||||
result = 31 * result + stopMonitoring.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
// override fun equals(other: Any?): Boolean {
|
||||
// if (this === other) return true
|
||||
// if (other !is Episode) return false
|
||||
// return id == other.id
|
||||
// }
|
||||
//
|
||||
// override fun hashCode(): Int {
|
||||
// val result = (id xor (id ushr 32)).toInt()
|
||||
// return result
|
||||
// }
|
||||
|
||||
enum class PlayState(val code: Int) {
|
||||
UNSPECIFIED(-2),
|
||||
NEW(-1),
|
||||
|
|
|
@ -4,6 +4,9 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
|||
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import io.realm.kotlin.types.RealmObject
|
||||
|
@ -141,6 +144,9 @@ class Feed : RealmObject {
|
|||
@Ignore
|
||||
var sortInfo: String = ""
|
||||
|
||||
@Ignore
|
||||
var isBuilding by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* This constructor is used for test purposes.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,568 @@
|
|||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.playback.base.VideoMode
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.RealmDB
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.AudioMediaTools
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.IntentUtils
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) {
|
||||
val TAG = this::class.simpleName ?: "ItemActionButton"
|
||||
|
||||
open val visibility: Boolean
|
||||
get() = true
|
||||
|
||||
var processing: Float = -1f
|
||||
|
||||
val actionState = mutableIntStateOf(0)
|
||||
|
||||
abstract fun getLabel(): Int
|
||||
|
||||
abstract fun getDrawable(): Int
|
||||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
@Composable
|
||||
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val label = getLabel()
|
||||
if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) {
|
||||
IconButton(onClick = {
|
||||
PlayActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_play_24dp), contentDescription = "Play") }
|
||||
}
|
||||
if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) {
|
||||
IconButton(onClick = {
|
||||
StreamActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_stream), contentDescription = "Stream") }
|
||||
}
|
||||
if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) {
|
||||
IconButton(onClick = {
|
||||
DownloadActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_download), contentDescription = "Download") }
|
||||
}
|
||||
if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) {
|
||||
IconButton(onClick = {
|
||||
DeleteActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_delete), contentDescription = "Delete") }
|
||||
}
|
||||
if (label != R.string.visit_website_label) {
|
||||
IconButton(onClick = {
|
||||
VisitWebsiteActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_web), contentDescription = "Web") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
companion object {
|
||||
fun forItem(episode: Episode): EpisodeActionButton {
|
||||
val media = episode.media ?: return TTSActionButton(episode)
|
||||
val isDownloadingMedia = when (media.downloadUrl) {
|
||||
null -> false
|
||||
else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
}
|
||||
Logd("ItemActionButton", "forItem: local feed: ${episode.feed?.isLocalFeed} downloaded: ${media.downloaded} playing: ${isCurrentlyPlaying(media)} ${episode.title} ")
|
||||
return when {
|
||||
isCurrentlyPlaying(media) -> PauseActionButton(episode)
|
||||
episode.feed != null && episode.feed!!.isLocalFeed -> PlayLocalActionButton(episode)
|
||||
media.downloaded -> PlayActionButton(episode)
|
||||
isDownloadingMedia -> CancelDownloadActionButton(episode)
|
||||
isStreamOverDownload || episode.feed == null || episode.feedId == null || episode.feed?.type == Feed.FeedType.YOUTUBE.name
|
||||
|| episode.feed?.preferences?.prefStreamOverDownload == true -> StreamActionButton(episode)
|
||||
else -> DownloadActionButton(episode)
|
||||
}
|
||||
}
|
||||
|
||||
fun playVideoIfNeeded(context: Context, media: Playable) {
|
||||
val item = (media as? EpisodeMedia)?.episode
|
||||
if (item?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
|
||||
&& videoPlayMode != VideoMode.AUDIO_ONLY.code && videoMode != VideoMode.AUDIO_ONLY
|
||||
&& media.getMediaType() == MediaType.VIDEO)
|
||||
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Boolean
|
||||
get() = if (item.link.isNullOrEmpty()) false else true
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.visit_website_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_web
|
||||
}
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (!item.link.isNullOrEmpty()) IntentUtils.openInBrowser(context, item.link!!)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
class CancelDownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
@StringRes
|
||||
override fun getLabel(): Int {
|
||||
return R.string.cancel_download_label
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_cancel
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
val media = item.media
|
||||
if (media != null) DownloadServiceInterface.get()?.cancel(context, media)
|
||||
if (UserPreferences.isEnableAutodownload) {
|
||||
val item_ = RealmDB.upsertBlk(item) {
|
||||
it.disableAutoDownload()
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item_))
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
Logd("PlayActionButton", "onClick called")
|
||||
val media = item.media
|
||||
if (media == null) {
|
||||
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
if (!media.fileExists()) {
|
||||
Toast.makeText(context, R.string.error_file_not_found, Toast.LENGTH_LONG).show()
|
||||
notifyMissingEpisodeMediaFile(context, media)
|
||||
return
|
||||
}
|
||||
if (PlaybackService.playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
PlaybackService.playbackService?.mPlayer?.resume()
|
||||
PlaybackService.playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
playVideoIfNeeded(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the database about a missing EpisodeMedia file. This method will correct the EpisodeMedia object's
|
||||
* values in the DB and send a FeedItemEvent.
|
||||
*/
|
||||
fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) {
|
||||
Logd(TAG, "notifyMissingEpisodeMediaFile called")
|
||||
Log.i(TAG, "The feedmanager was notified about a missing episode. It will update its database now.")
|
||||
val episode = RealmDB.realm.query(Episode::class).query("id == media.id").first().find()
|
||||
// val episode = media.episodeOrFetch()
|
||||
if (episode != null) {
|
||||
val episode_ = RealmDB.upsertBlk(episode) {
|
||||
// it.media = media
|
||||
it.media?.downloaded = false
|
||||
it.media?.fileUrl = null
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.removed(episode_))
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.error_file_not_found)))
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
override val visibility: Boolean
|
||||
get() {
|
||||
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.delete_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
LocalDeleteModal.deleteEpisodesWarnLocal(context, listOf(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
class PauseActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.pause_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_pause
|
||||
}
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
Logd("PauseActionButton", "onClick called")
|
||||
val media = item.media ?: return
|
||||
|
||||
if (isCurrentlyPlaying(media)) context.sendBroadcast(MediaButtonReceiver.createIntent(context,
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
// EventFlow.postEvent(FlowEvent.PlayEvent(item, Action.END))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Boolean
|
||||
get() = if (item.feed?.isLocalFeed == true) false else true
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.download_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (shouldNotDownload(item.media)) return
|
||||
UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD)
|
||||
if (NetworkUtils.isEpisodeDownloadAllowed) DownloadServiceInterface.get()?.downloadNow(context, item, false)
|
||||
else {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.confirm_mobile_download_dialog_title)
|
||||
.setPositiveButton(R.string.confirm_mobile_download_dialog_download_later) { _: DialogInterface?, _: Int ->
|
||||
DownloadServiceInterface.get()?.downloadNow(context, item, false) }
|
||||
.setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time) { _: DialogInterface?, _: Int ->
|
||||
DownloadServiceInterface.get()?.downloadNow(context, item, true) }
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
if (NetworkUtils.isNetworkRestricted && NetworkUtils.isVpnOverWifi) builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn)
|
||||
else builder.setMessage(R.string.confirm_mobile_download_dialog_message)
|
||||
|
||||
builder.show()
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
private fun shouldNotDownload(media: EpisodeMedia?): Boolean {
|
||||
if (media?.downloadUrl == null) return true
|
||||
val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
return isDownloading || media.downloaded
|
||||
}
|
||||
}
|
||||
|
||||
class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.stream_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_stream
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
if (item.media == null) return
|
||||
// Logd("StreamActionButton", "item.feed: ${item.feedId}")
|
||||
val media = if (item.feedId != null) item.media!! else RemoteMedia(item)
|
||||
UsageStatistics.logAction(UsageStatistics.ACTION_STREAM)
|
||||
if (!NetworkUtils.isStreamingAllowed) {
|
||||
StreamingConfirmationDialog(context, media).show()
|
||||
return
|
||||
}
|
||||
stream(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) {
|
||||
@UnstableApi
|
||||
fun show() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.stream_label)
|
||||
.setMessage(R.string.confirm_mobile_streaming_notification_message)
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> stream(context, playable) }
|
||||
.setNegativeButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
|
||||
NetworkUtils.isAllowMobileStreaming = true
|
||||
stream(context, playable)
|
||||
}
|
||||
.setNeutralButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun stream(context: Context, media: Playable) {
|
||||
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()
|
||||
if (media is EpisodeMedia && media.episode != null) EventFlow.postEvent(FlowEvent.PlayEvent(media.episode!!))
|
||||
playVideoIfNeeded(context, media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
private var readerText: String? = null
|
||||
|
||||
override val visibility: Boolean
|
||||
get() = if (item.link.isNullOrEmpty()) false else true
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.TTS_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.text_to_speech
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onClick(context: Context) {
|
||||
Logd("TTSActionButton", "onClick called")
|
||||
if (item.link.isNullOrEmpty()) {
|
||||
Toast.makeText(context, R.string.episode_has_no_content, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
processing = 0.01f
|
||||
item.setBuilding()
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
RealmDB.runOnIOScope {
|
||||
if (item.transcript == null) {
|
||||
val url = item.link!!
|
||||
val htmlSource = NetworkUtils.fetchHtmlSource(url)
|
||||
val article = Readability4J(item.link!!, htmlSource).parse()
|
||||
readerText = article.textContent
|
||||
item = RealmDB.upsertBlk(item) {
|
||||
it.setTranscriptIfLonger(article.contentWithDocumentsCharsetOrUtf8)
|
||||
}
|
||||
// persistEpisode(item)
|
||||
Logd(TAG,
|
||||
"readability4J: ${readerText?.substring(max(0, readerText!!.length - 100), readerText!!.length)}")
|
||||
} else readerText = HtmlCompat.fromHtml(item.transcript!!, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
processing = 0.1f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
if (!readerText.isNullOrEmpty()) {
|
||||
while (!FeedEpisodesFragment.ttsReady) runBlocking { delay(100) }
|
||||
|
||||
processing = 0.15f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
while (FeedEpisodesFragment.ttsWorking) runBlocking { delay(100) }
|
||||
FeedEpisodesFragment.ttsWorking = true
|
||||
if (item.feed?.language != null) {
|
||||
val result = FeedEpisodesFragment.tts?.setLanguage(Locale(item.feed!!.language!!))
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "TTS language not supported ${item.feed!!.language} $result")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.language_not_supported_by_tts) + " ${item.feed!!.language} $result",
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var j = 0
|
||||
val mediaFile = File(FilesUtils.getMediafilePath(item), FilesUtils.getMediafilename(item))
|
||||
FeedEpisodesFragment.tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onDone(utteranceId: String?) {
|
||||
j++
|
||||
Logd(TAG, "onDone ${mediaFile.length()} $utteranceId")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onError(utteranceId: String) {
|
||||
Log.e(TAG, "onError utterance error: $utteranceId")
|
||||
Log.e(TAG, "onError $readerText")
|
||||
}
|
||||
|
||||
override fun onError(utteranceId: String, errorCode: Int) {
|
||||
Log.e(TAG, "onError1 utterance error: $utteranceId $errorCode")
|
||||
Log.e(TAG, "onError1 $readerText")
|
||||
}
|
||||
})
|
||||
|
||||
Logd(TAG, "readerText: ${readerText?.length}")
|
||||
var startIndex = 0
|
||||
var i = 0
|
||||
val parts = mutableListOf<String>()
|
||||
val chunkLength = TextToSpeech.getMaxSpeechInputLength()
|
||||
var status = TextToSpeech.ERROR
|
||||
while (startIndex < readerText!!.length) {
|
||||
Logd(TAG, "working on chunk $i $startIndex")
|
||||
val endIndex = minOf(startIndex + chunkLength, readerText!!.length)
|
||||
val chunk = readerText!!.substring(startIndex, endIndex)
|
||||
val tempFile = File.createTempFile("tts_temp_${i}_", ".wav")
|
||||
parts.add(tempFile.absolutePath)
|
||||
status =
|
||||
FeedEpisodesFragment.tts?.synthesizeToFile(chunk, null, tempFile, tempFile.absolutePath) ?: 0
|
||||
Logd(TAG, "status: $status chunk: ${chunk.substring(0, min(80, chunk.length))}")
|
||||
if (status == TextToSpeech.ERROR) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context,
|
||||
"Error generating audio file $tempFile.absolutePath",
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
break
|
||||
}
|
||||
startIndex += chunkLength
|
||||
i++
|
||||
while (i - j > 0) runBlocking { delay(100) }
|
||||
processing = 0.15f + 0.7f * startIndex / readerText!!.length
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
}
|
||||
processing = 0.85f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
AudioMediaTools.mergeAudios(parts.toTypedArray(), mediaFile.absolutePath, null)
|
||||
|
||||
val mFilename = mediaFile.absolutePath
|
||||
Logd(TAG, "saving TTS to file $mFilename")
|
||||
val media = EpisodeMedia(item, null, 0, "audio/*")
|
||||
media.fileUrl = mFilename
|
||||
// media.downloaded = true
|
||||
media.setIsDownloaded()
|
||||
item = RealmDB.upsertBlk(item) {
|
||||
it.media = media
|
||||
it.setTranscriptIfLonger(readerText)
|
||||
}
|
||||
// persistEpisode(item)
|
||||
}
|
||||
for (p in parts) {
|
||||
val f = File(p)
|
||||
f.delete()
|
||||
}
|
||||
FeedEpisodesFragment.ttsWorking = false
|
||||
} else withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context,
|
||||
R.string.episode_has_no_content,
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
item.setPlayed(false)
|
||||
processing = 1f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
Logd("PlayLocalActionButton", "onClick called")
|
||||
val media = item.media
|
||||
if (media == null) {
|
||||
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
if (PlaybackService.playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
PlaybackService.playbackService?.mPlayer?.resume()
|
||||
PlaybackService.playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context,
|
||||
MediaType.VIDEO))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Boolean
|
||||
get() = if (item.isPlayed()) false else true
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label)
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_check
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onClick(context: Context) {
|
||||
if (!item.isPlayed()) Episodes.setPlayState(Episode.PlayState.PLAYED.code, true, item)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.handler
|
||||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.handler
|
||||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.handler
|
||||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.AttrRes
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
package ac.mdiq.podcini.ui.actions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
|
@ -18,8 +18,7 @@ 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.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction.Companion.NO_ACTION
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction.Companion.NO_ACTION
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog
|
||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
|
@ -1,37 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class CancelDownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
@StringRes
|
||||
override fun getLabel(): Int {
|
||||
return R.string.cancel_download_label
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_cancel
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media
|
||||
if (media != null) DownloadServiceInterface.get()?.cancel(context, media)
|
||||
if (isEnableAutodownload) {
|
||||
val item_ = upsertBlk(item) {
|
||||
it.disableAutoDownload()
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item_))
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() {
|
||||
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return View.VISIBLE
|
||||
return View.INVISIBLE
|
||||
}
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.delete_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
deleteEpisodesWarnLocal(context, listOf(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.View
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeDownloadAllowed
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
|
||||
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.download_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (shouldNotDownload(item.media)) return
|
||||
logAction(UsageStatistics.ACTION_DOWNLOAD)
|
||||
if (isEpisodeDownloadAllowed) DownloadServiceInterface.get()?.downloadNow(context, item, false)
|
||||
else {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.confirm_mobile_download_dialog_title)
|
||||
.setPositiveButton(R.string.confirm_mobile_download_dialog_download_later) { _: DialogInterface?, _: Int ->
|
||||
DownloadServiceInterface.get()?.downloadNow(context, item, false) }
|
||||
.setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time) { _: DialogInterface?, _: Int ->
|
||||
DownloadServiceInterface.get()?.downloadNow(context, item, true) }
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
if (isNetworkRestricted && isVpnOverWifi) builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn)
|
||||
else builder.setMessage(R.string.confirm_mobile_download_dialog_message)
|
||||
|
||||
builder.show()
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
private fun shouldNotDownload(media: EpisodeMedia?): Boolean {
|
||||
if (media?.downloadUrl == null) return true
|
||||
val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
return isDownloading || media.downloaded
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.playback.base.VideoMode
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) {
|
||||
val TAG = this::class.simpleName ?: "ItemActionButton"
|
||||
|
||||
open val visibility: Int
|
||||
get() = View.VISIBLE
|
||||
|
||||
var processing: Float = -1f
|
||||
|
||||
val actionState = mutableIntStateOf(0)
|
||||
|
||||
abstract fun getLabel(): Int
|
||||
|
||||
abstract fun getDrawable(): Int
|
||||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
// fun configure(button: View, icon: ImageView, context: Context) {
|
||||
// button.visibility = visibility
|
||||
// button.contentDescription = context.getString(getLabel())
|
||||
// button.setOnClickListener { onClick(context) }
|
||||
// button.setOnLongClickListener {
|
||||
// val composeView = ComposeView(context).apply {
|
||||
// setContent {
|
||||
// val showDialog = remember { mutableStateOf(true) }
|
||||
// CustomTheme(context) { AltActionsDialog(context, showDialog.value, onDismiss = { showDialog.value = false }) }
|
||||
// }
|
||||
// }
|
||||
// (button as? ViewGroup)?.addView(composeView)
|
||||
// true
|
||||
// }
|
||||
// icon.setImageResource(getDrawable())
|
||||
// }
|
||||
|
||||
@Composable
|
||||
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val label = getLabel()
|
||||
if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) {
|
||||
IconButton(onClick = {
|
||||
PlayActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_play_24dp), contentDescription = "Play") }
|
||||
}
|
||||
if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) {
|
||||
IconButton(onClick = {
|
||||
StreamActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_stream), contentDescription = "Stream") }
|
||||
}
|
||||
if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) {
|
||||
IconButton(onClick = {
|
||||
DownloadActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_download), contentDescription = "Download") }
|
||||
}
|
||||
if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) {
|
||||
IconButton(onClick = {
|
||||
DeleteActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_delete), contentDescription = "Delete") }
|
||||
}
|
||||
if (label != R.string.visit_website_label) {
|
||||
IconButton(onClick = {
|
||||
VisitWebsiteActionButton(item).onClick(context)
|
||||
onDismiss()
|
||||
}) { Image(painter = painterResource(R.drawable.ic_web), contentDescription = "Web") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
companion object {
|
||||
fun forItem(episode: Episode): EpisodeActionButton {
|
||||
val media = episode.media ?: return TTSActionButton(episode)
|
||||
val isDownloadingMedia = when (media.downloadUrl) {
|
||||
null -> false
|
||||
else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
}
|
||||
Logd("ItemActionButton", "forItem: ${episode.feedId} ${episode.feed?.isLocalFeed} ${media.downloaded} ${isCurrentlyPlaying(media)} ${episode.title} ")
|
||||
return when {
|
||||
isCurrentlyPlaying(media) -> PauseActionButton(episode)
|
||||
episode.feed != null && episode.feed!!.isLocalFeed -> PlayLocalActionButton(episode)
|
||||
media.downloaded -> PlayActionButton(episode)
|
||||
isDownloadingMedia -> CancelDownloadActionButton(episode)
|
||||
isStreamOverDownload || episode.feed == null || episode.feedId == null || episode.feed?.type == Feed.FeedType.YOUTUBE.name
|
||||
|| episode.feed?.preferences?.prefStreamOverDownload == true -> StreamActionButton(episode)
|
||||
else -> DownloadActionButton(episode)
|
||||
}
|
||||
}
|
||||
|
||||
fun playVideoIfNeeded(context: Context, media: Playable) {
|
||||
val item = (media as? EpisodeMedia)?.episode
|
||||
if (item?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
|
||||
&& videoPlayMode != VideoMode.AUDIO_ONLY.code && videoMode != VideoMode.AUDIO_ONLY
|
||||
&& media.getMediaType() == MediaType.VIDEO)
|
||||
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label)
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_check
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
if (!item.isPlayed()) setPlayState(Episode.PlayState.PLAYED.code, true, item)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class PauseActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.pause_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_pause
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
Logd("PauseActionButton", "onClick called")
|
||||
val media = item.media ?: return
|
||||
|
||||
if (isCurrentlyPlaying(media)) context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
// EventFlow.postEvent(FlowEvent.PlayEvent(item, Action.END))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.clearCurTempSpeed
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
Logd("PlayActionButton", "onClick called")
|
||||
val media = item.media
|
||||
if (media == null) {
|
||||
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
if (!media.fileExists()) {
|
||||
Toast.makeText(context, R.string.error_file_not_found, Toast.LENGTH_LONG).show()
|
||||
notifyMissingEpisodeMediaFile(context, media)
|
||||
return
|
||||
}
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
playVideoIfNeeded(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the database about a missing EpisodeMedia file. This method will correct the EpisodeMedia object's
|
||||
* values in the DB and send a FeedItemEvent.
|
||||
*/
|
||||
fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) {
|
||||
Logd(TAG, "notifyMissingEpisodeMediaFile called")
|
||||
Log.i(TAG, "The feedmanager was notified about a missing episode. It will update its database now.")
|
||||
val episode = realm.query(Episode::class).query("id == media.id").first().find()
|
||||
// val episode = media.episodeOrFetch()
|
||||
if (episode != null) {
|
||||
val episode_ = upsertBlk(episode) {
|
||||
// it.media = media
|
||||
it.media?.downloaded = false
|
||||
it.media?.fileUrl = null
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.removed(episode_))
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.error_file_not_found)))
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.clearCurTempSpeed
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
Logd("PlayLocalActionButton", "onClick called")
|
||||
val media = item.media
|
||||
if (media == null) {
|
||||
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.clearCurTempSpeed
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.model.RemoteMedia
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.stream_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_stream
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
if (item.media == null) return
|
||||
// Logd("StreamActionButton", "item.feed: ${item.feedId}")
|
||||
val media = if (item.feedId != null) item.media!! else RemoteMedia(item)
|
||||
logAction(UsageStatistics.ACTION_STREAM)
|
||||
if (!isStreamingAllowed) {
|
||||
StreamingConfirmationDialog(context, media).show()
|
||||
return
|
||||
}
|
||||
stream(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) {
|
||||
@UnstableApi
|
||||
fun show() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.stream_label)
|
||||
.setMessage(R.string.confirm_mobile_streaming_notification_message)
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> stream(context, playable) }
|
||||
.setNegativeButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
|
||||
isAllowMobileStreaming = true
|
||||
stream(context, playable)
|
||||
}
|
||||
.setNeutralButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun stream(context: Context, media: Playable) {
|
||||
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()
|
||||
if (media is EpisodeMedia && media.episode != null) EventFlow.postEvent(FlowEvent.PlayEvent(media.episode!!))
|
||||
playVideoIfNeeded(context, media)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.utils.AudioMediaTools.mergeAudios
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilePath
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.tts
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsReady
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.TextToSpeech.getMaxSpeechInputLength
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
private var readerText: String? = null
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.TTS_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.text_to_speech
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onClick(context: Context) {
|
||||
Logd("TTSActionButton", "onClick called")
|
||||
if (item.link.isNullOrEmpty()) {
|
||||
Toast.makeText(context, R.string.episode_has_no_content, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
processing = 0.01f
|
||||
item.setBuilding()
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
runOnIOScope {
|
||||
if (item.transcript == null) {
|
||||
val url = item.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val article = Readability4J(item.link!!, htmlSource).parse()
|
||||
readerText = article.textContent
|
||||
item = upsertBlk(item) {
|
||||
it.setTranscriptIfLonger(article.contentWithDocumentsCharsetOrUtf8)
|
||||
}
|
||||
// persistEpisode(item)
|
||||
Logd(TAG, "readability4J: ${readerText?.substring(max(0, readerText!!.length-100), readerText!!.length)}")
|
||||
} else readerText = HtmlCompat.fromHtml(item.transcript!!, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
processing = 0.1f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
if (!readerText.isNullOrEmpty()) {
|
||||
while (!ttsReady) runBlocking { delay(100) }
|
||||
|
||||
processing = 0.15f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
while (ttsWorking) runBlocking { delay(100) }
|
||||
ttsWorking = true
|
||||
if (item.feed?.language != null) {
|
||||
val result = tts?.setLanguage(Locale(item.feed!!.language!!))
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "TTS language not supported ${item.feed!!.language} $result")
|
||||
withContext(Dispatchers.Main) { Toast.makeText(context, context.getString(R.string.language_not_supported_by_tts) + " ${item.feed!!.language} $result", Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
var j = 0
|
||||
val mediaFile = File(getMediafilePath(item), getMediafilename(item))
|
||||
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onDone(utteranceId: String?) {
|
||||
j++
|
||||
Logd(TAG, "onDone ${mediaFile.length()} $utteranceId")
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onError(utteranceId: String) {
|
||||
Log.e(TAG, "onError utterance error: $utteranceId")
|
||||
Log.e(TAG, "onError $readerText")
|
||||
}
|
||||
override fun onError(utteranceId: String, errorCode: Int) {
|
||||
Log.e(TAG, "onError1 utterance error: $utteranceId $errorCode")
|
||||
Log.e(TAG, "onError1 $readerText")
|
||||
}
|
||||
})
|
||||
|
||||
Logd(TAG, "readerText: ${readerText?.length}")
|
||||
var startIndex = 0
|
||||
var i = 0
|
||||
val parts = mutableListOf<String>()
|
||||
val chunkLength = getMaxSpeechInputLength()
|
||||
var status = TextToSpeech.ERROR
|
||||
while (startIndex < readerText!!.length) {
|
||||
Logd(TAG, "working on chunk $i $startIndex")
|
||||
val endIndex = minOf(startIndex + chunkLength, readerText!!.length)
|
||||
val chunk = readerText!!.substring(startIndex, endIndex)
|
||||
val tempFile = File.createTempFile("tts_temp_${i}_", ".wav")
|
||||
parts.add(tempFile.absolutePath)
|
||||
status = tts?.synthesizeToFile(chunk, null, tempFile, tempFile.absolutePath) ?: 0
|
||||
Logd(TAG, "status: $status chunk: ${chunk.substring(0, min(80, chunk.length))}")
|
||||
if (status == TextToSpeech.ERROR) {
|
||||
withContext(Dispatchers.Main) { Toast.makeText(context, "Error generating audio file $tempFile.absolutePath", Toast.LENGTH_LONG).show() }
|
||||
break
|
||||
}
|
||||
startIndex += chunkLength
|
||||
i++
|
||||
while (i-j > 0) runBlocking { delay(100) }
|
||||
processing = 0.15f + 0.7f * startIndex / readerText!!.length
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
}
|
||||
processing = 0.85f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
mergeAudios(parts.toTypedArray(), mediaFile.absolutePath, null)
|
||||
|
||||
val mFilename = mediaFile.absolutePath
|
||||
Logd(TAG, "saving TTS to file $mFilename")
|
||||
val media = EpisodeMedia(item, null, 0, "audio/*")
|
||||
media.fileUrl = mFilename
|
||||
// media.downloaded = true
|
||||
media.setIsDownloaded()
|
||||
item = upsertBlk(item) {
|
||||
it.media = media
|
||||
it.setTranscriptIfLonger(readerText)
|
||||
}
|
||||
// persistEpisode(item)
|
||||
}
|
||||
for (p in parts) {
|
||||
val f = File(p)
|
||||
f.delete()
|
||||
}
|
||||
ttsWorking = false
|
||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.episode_has_no_content, Toast.LENGTH_LONG).show() }
|
||||
|
||||
item.setPlayed(false)
|
||||
processing = 1f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
||||
class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.visit_website_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_web
|
||||
}
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import ac.mdiq.podcini.receiver.PlayerWidget
|
|||
import ac.mdiq.podcini.storage.database.Feeds.buildTags
|
||||
import ac.mdiq.podcini.storage.database.Feeds.monitorFeeds
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
|
@ -65,7 +65,6 @@ import androidx.core.view.WindowCompat
|
|||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.MediaController
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package ac.mdiq.podcini.ui.activity
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.confirmAddYoutubeEpisode
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
@ -16,20 +14,11 @@ import android.util.Log
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
|
||||
class ShareReceiverActivity : AppCompatActivity() {
|
||||
|
@ -54,6 +43,7 @@ class ShareReceiverActivity : AppCompatActivity() {
|
|||
if (urlString != null) sharedUrl = URLDecoder.decode(urlString, "UTF-8")
|
||||
}
|
||||
Logd(TAG, "feedUrl: $sharedUrl")
|
||||
val url = URL(sharedUrl)
|
||||
when {
|
||||
// plain text
|
||||
sharedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> {
|
||||
|
@ -62,12 +52,13 @@ class ShareReceiverActivity : AppCompatActivity() {
|
|||
finish()
|
||||
}
|
||||
// Youtube media
|
||||
sharedUrl!!.startsWith("https://youtube.com/watch?") || sharedUrl!!.startsWith("https://music.youtube.com/watch?") -> {
|
||||
// sharedUrl!!.startsWith("https://youtube.com/watch?") || sharedUrl!!.startsWith("https://www.youtube.com/watch?") || sharedUrl!!.startsWith("https://music.youtube.com/watch?") -> {
|
||||
(isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url) -> {
|
||||
Logd(TAG, "got youtube media")
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(this@ShareReceiverActivity) {
|
||||
confirmAddEpisode(showDialog.value, onDismissRequest = {
|
||||
confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
|
||||
showDialog.value = false
|
||||
finish()
|
||||
})
|
||||
|
@ -85,49 +76,44 @@ class ShareReceiverActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun confirmAddEpisode(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(align = Alignment.Center)
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.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(),
|
||||
)
|
||||
}
|
||||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val info = StreamInfo.getInfo(Vista.getService(0), sharedUrl!!)
|
||||
Logd(TAG, "info: $info")
|
||||
val episode = episodeFromStreamInfo(info)
|
||||
Logd(TAG, "episode: $episode")
|
||||
addToYoutubeSyndicate(episode, !audioOnly)
|
||||
}
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @Composable
|
||||
// fun confirmAddEpisode(sharedUrl: String, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
// var showToast by remember { mutableStateOf(false) }
|
||||
// var toastMassege by remember { mutableStateOf("")}
|
||||
// if (showToast) CustomToast(message = toastMassege, onDismiss = { showToast = false })
|
||||
//
|
||||
// if (showDialog) {
|
||||
// Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
// Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
// Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.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())
|
||||
// }
|
||||
// Button(onClick = {
|
||||
// CoroutineScope(Dispatchers.IO).launch {
|
||||
// try {
|
||||
// val info = StreamInfo.getInfo(Vista.getService(0), sharedUrl)
|
||||
// Logd(TAG, "info: $info")
|
||||
// val episode = episodeFromStreamInfo(info)
|
||||
// Logd(TAG, "episode: $episode")
|
||||
// addToYoutubeSyndicate(episode, !audioOnly)
|
||||
// } catch (e: Throwable) {
|
||||
// toastMassege = "Receive share error: ${e.message}"
|
||||
// Log.e(TAG, toastMassege)
|
||||
// showToast = true
|
||||
// }
|
||||
// }
|
||||
// onDismissRequest()
|
||||
// }) {
|
||||
// Text("Confirm")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
runOnUiThread {
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
@ -43,3 +53,19 @@ fun Spinner(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () -> Unit) {
|
||||
// Launch a coroutine to auto-dismiss the toast after a certain time
|
||||
LaunchedEffect(message) {
|
||||
delay(durationMillis)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// Box to display the toast message at the bottom of the screen
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.BottomCenter) {
|
||||
Box(modifier = Modifier.background(Color.Black, RoundedCornerShape(8.dp)).padding(8.dp)) {
|
||||
Text(text = message, color = Color.White, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,8 +7,10 @@ import ac.mdiq.podcini.playback.base.InTheatre
|
|||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.status
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
|
@ -16,16 +18,21 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler.PutToQueueDialog
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler.PutToQueueDialog
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
|
@ -36,6 +43,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
|
@ -57,14 +65,13 @@ import androidx.compose.ui.res.vectorResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import coil.compose.AsyncImage
|
||||
import io.realm.kotlin.notifications.SingleQueryChange
|
||||
import io.realm.kotlin.notifications.UpdatedObject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.URL
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
|
@ -86,10 +93,37 @@ fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction?>,
|
|||
|
||||
var queueChanged by mutableIntStateOf(0)
|
||||
|
||||
@Stable
|
||||
class EpisodeVM(var episode: Episode) {
|
||||
var positionState by mutableStateOf(episode.media?.position?:0)
|
||||
var playedState by mutableStateOf(episode.isPlayed())
|
||||
var isPlayingState by mutableStateOf(false)
|
||||
var farvoriteState by mutableStateOf(episode.isFavorite)
|
||||
var inProgressState by mutableStateOf(episode.isInProgress)
|
||||
var downloadState by mutableIntStateOf(if (episode.media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
||||
var isRemote by mutableStateOf(false)
|
||||
var actionButton by mutableStateOf<EpisodeActionButton?>(null)
|
||||
var actionRes by mutableIntStateOf(R.drawable.ic_questionmark)
|
||||
var showAltActionsDialog by mutableStateOf(false)
|
||||
var dlPercent by mutableIntStateOf(0)
|
||||
var inQueueState by mutableStateOf(curQueue.contains(episode))
|
||||
var isSelected by mutableStateOf(false)
|
||||
var prog by mutableFloatStateOf(0f)
|
||||
|
||||
var episodeMonitor: Job? by mutableStateOf(null)
|
||||
var mediaMonitor: Job? by mutableStateOf(null)
|
||||
|
||||
fun stopMonitoring() {
|
||||
episodeMonitor?.cancel()
|
||||
mediaMonitor?.cancel()
|
||||
Logd("EpisodeVM", "cancel monitoring")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, refreshCB: (()->Unit)? = null,
|
||||
leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null, actionButton_: ((Episode)->EpisodeActionButton)? = null) {
|
||||
fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>, refreshCB: (()->Unit)? = null,
|
||||
leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null, actionButton_: ((Episode)-> EpisodeActionButton)? = null) {
|
||||
val TAG = "EpisodeLazyColumn"
|
||||
var selectMode by remember { mutableStateOf(false) }
|
||||
var selectedSize by remember { mutableStateOf(0) }
|
||||
|
@ -97,10 +131,16 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
var longPressIndex by remember { mutableIntStateOf(-1) }
|
||||
val dls = remember { DownloadServiceInterface.get() }
|
||||
|
||||
val showConfirmYoutubeDialog = remember { mutableStateOf(false) }
|
||||
val youtubeUrls = remember { mutableListOf<String>() }
|
||||
confirmAddYoutubeEpisode(youtubeUrls, showConfirmYoutubeDialog.value, onDismissRequest = {
|
||||
showConfirmYoutubeDialog.value = false
|
||||
})
|
||||
|
||||
@Composable
|
||||
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
||||
val TAG = "EpisodeSpeedDial ${selected.size}"
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
val options = mutableListOf<@Composable () -> Unit>(
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
@ -193,7 +233,18 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
selectMode = false
|
||||
Logd(TAG, "reserve: ${selected.size}")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
for (e in selected) { addToMiscSyndicate(e) }
|
||||
youtubeUrls.clear()
|
||||
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)) {
|
||||
youtubeUrls.add(e.media!!.downloadUrl!!)
|
||||
} else addToMiscSyndicate(e)
|
||||
}
|
||||
Logd(TAG, "youtubeUrls: ${youtubeUrls.size}")
|
||||
withContext(Dispatchers.Main) {
|
||||
showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
@ -216,66 +267,53 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
var refreshing by remember { mutableStateOf(false)}
|
||||
|
||||
PullToRefreshBox(modifier = Modifier.fillMaxWidth(), isRefreshing = refreshing, indicator = {}, onRefresh = {
|
||||
// coroutineScope.launch {
|
||||
refreshing = true
|
||||
refreshCB?.invoke()
|
||||
// if (swipeRefresh) FeedUpdateManager.runOnceOrAsk(activity)
|
||||
refreshing = false
|
||||
// }
|
||||
}) {
|
||||
LazyColumn(state = lazyListState,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||
LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
itemsIndexed(episodes, key = {index, episode -> episode.id}) { index, episode ->
|
||||
var positionState by remember { mutableStateOf(episode.media?.position?:0) }
|
||||
var playedState by remember { mutableStateOf(episode.isPlayed()) }
|
||||
var farvoriteState by remember { mutableStateOf(episode.isFavorite) }
|
||||
var inProgressState by remember { mutableStateOf(episode.isInProgress) }
|
||||
|
||||
var episodeMonitor: Job? by remember { mutableStateOf(null) }
|
||||
var mediaMonitor: Job? by remember { mutableStateOf(null) }
|
||||
if (episodeMonitor == null) {
|
||||
episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val item_ = realm.query(Episode::class).query("id == ${episode.id}").first()
|
||||
Logd(TAG, "start monitoring episode: $index ${episode.title}")
|
||||
itemsIndexed(vms, key = {index, vm -> vm.episode.id}) { index, vm ->
|
||||
if (vm.episodeMonitor == null) {
|
||||
vm.episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val item_ = realm.query(Episode::class).query("id == ${vm.episode.id}").first()
|
||||
Logd(TAG, "start monitoring episode: $index ${vm.episode.title}")
|
||||
val episodeFlow = item_.asFlow()
|
||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "episodeMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
Logd(TAG, "episodeMonitor $index ${changes.obj.id} ${episodes[index].id} ${episode.id}")
|
||||
if (index < episodes.size && episodes[index].id == changes.obj.id) {
|
||||
playedState = changes.obj.isPlayed()
|
||||
farvoriteState = changes.obj.isFavorite
|
||||
episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
changes.obj.copyStates(episodes[index])
|
||||
// remove action could possibly conflict with the one in mediaMonitor
|
||||
// episodes.removeAt(index)
|
||||
// episodes.add(index, changes.obj)
|
||||
}
|
||||
if (index < vms.size && vms[index].episode.id == changes.obj.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
vms[index].playedState = changes.obj.isPlayed()
|
||||
vms[index].farvoriteState = changes.obj.isFavorite
|
||||
vms[index].episode = changes.obj // direct assignment doesn't update member like media??
|
||||
}
|
||||
Logd(TAG, "episodeMonitor $index ${vms[index].playedState} ${vm.playedState} ")
|
||||
} else Logd(TAG, "episodeMonitor index out bound: $index ${vms.size}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mediaMonitor == null) {
|
||||
mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val item_ = realm.query(Episode::class).query("id == ${episode.id}").first()
|
||||
Logd(TAG, "start monitoring media: $index ${episode.title}")
|
||||
if (vm.mediaMonitor == null) {
|
||||
vm.mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val item_ = realm.query(Episode::class).query("id == ${vm.episode.id}").first()
|
||||
Logd(TAG, "start monitoring media: $index ${vm.episode.title}")
|
||||
val episodeFlow = item_.asFlow(listOf("media.*"))
|
||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "mediaMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
if (index < episodes.size && episodes[index].id == changes.obj.id) {
|
||||
positionState = changes.obj.media?.position ?: 0
|
||||
inProgressState = changes.obj.isInProgress
|
||||
// episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
changes.obj.copyStates(episodes[index])
|
||||
episodes.removeAt(index)
|
||||
episodes.add(index, changes.obj)
|
||||
}
|
||||
if (index < vms.size && vms[index].episode.id == changes.obj.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
vms[index].positionState = changes.obj.media?.position ?: 0
|
||||
vms[index].inProgressState = changes.obj.isInProgress
|
||||
Logd(TAG, "mediaMonitor $index ${vm.positionState} ${vm.inProgressState} ${vm.episode.title}")
|
||||
vms[index].episode = changes.obj
|
||||
}
|
||||
} else Logd(TAG, "mediaMonitor index out bound: $index ${vms.size}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
@ -285,14 +323,16 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
Logd(TAG, "cancelling monitoring $index")
|
||||
episodeMonitor?.cancel()
|
||||
mediaMonitor?.cancel()
|
||||
vm.episodeMonitor?.cancel()
|
||||
vm.mediaMonitor?.cancel()
|
||||
}
|
||||
}
|
||||
if (episodes[index].stopMonitoring.value) {
|
||||
Logd(TAG, "cancel monitoring: ${episodes[index].title}")
|
||||
episodeMonitor?.cancel()
|
||||
mediaMonitor?.cancel()
|
||||
LaunchedEffect(vm.actionButton) {
|
||||
Logd(TAG, "LaunchedEffect init actionButton")
|
||||
if (vm.actionButton == null) {
|
||||
vm.actionButton = if (actionButton_ != null) actionButton_(vm.episode) else EpisodeActionButton.forItem(vm.episode)
|
||||
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||
}
|
||||
}
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
|
@ -308,8 +348,11 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
coroutineScope.launch {
|
||||
val velocity = velocityTracker.calculateVelocity().x
|
||||
if (velocity > 1000f || velocity < -1000f) {
|
||||
if (velocity > 0) rightSwipeCB?.invoke(episodes[index])
|
||||
else leftSwipeCB?.invoke(episodes[index])
|
||||
Logd(TAG, "velocity: $velocity")
|
||||
// if (velocity > 0) rightSwipeCB?.invoke(vms[index].episode)
|
||||
// else leftSwipeCB?.invoke(vms[index].episode)
|
||||
if (velocity > 0) rightSwipeCB?.invoke(vm.episode)
|
||||
else leftSwipeCB?.invoke(vm.episode)
|
||||
}
|
||||
offsetX.animateTo(
|
||||
targetValue = 0f, // Back to the initial position
|
||||
|
@ -320,18 +363,17 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
)
|
||||
}.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
||||
) {
|
||||
var isSelected by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = selectMode, key2 = selectedSize) {
|
||||
isSelected = selectMode && episode in selected
|
||||
vm.isSelected = selectMode && vm.episode in selected
|
||||
// Logd(TAG, "LaunchedEffect $index $isSelected ${selected.size}")
|
||||
}
|
||||
fun toggleSelected() {
|
||||
isSelected = !isSelected
|
||||
if (isSelected) selected.add(episodes[index])
|
||||
else selected.remove(episodes[index])
|
||||
vm.isSelected = !vm.isSelected
|
||||
if (vm.isSelected) selected.add(vms[index].episode)
|
||||
else selected.remove(vms[index].episode)
|
||||
}
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Row (Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
Row (Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
if (false) {
|
||||
val typedValue = TypedValue()
|
||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
||||
|
@ -341,7 +383,7 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
}
|
||||
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||
val (imgvCover, checkMark) = createRefs()
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode)
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(56.dp).height(56.dp)
|
||||
|
@ -352,10 +394,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
}.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
if (selectMode) toggleSelected()
|
||||
else activity.loadChildFragment(FeedInfoFragment.newInstance(episode.feed!!))
|
||||
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
|
||||
}))
|
||||
val alpha = if (playedState) 1.0f else 0f
|
||||
if (playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||
val alpha = if (vm.playedState) 1.0f else 0f
|
||||
if (vm.playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
|
@ -363,84 +405,84 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
}
|
||||
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
|
||||
.combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${episode.title}")
|
||||
Logd(TAG, "clicked: ${vm.episode.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(episode))
|
||||
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
|
||||
}, onLongClick = {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
vm.isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(episodes[index])
|
||||
selected.add(vms[index].episode)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selected.clear()
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
Logd(TAG, "long clicked: ${episode.title}")
|
||||
Logd(TAG, "long clicked: ${vm.episode.title}")
|
||||
})) {
|
||||
var inQueueState by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = queueChanged) {
|
||||
if (index>=episodes.size) return@LaunchedEffect
|
||||
inQueueState = curQueue.contains(episodes[index])
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
vms[index].inQueueState = curQueue.contains(vms[index].episode)
|
||||
}
|
||||
val dur = episode.media!!.getDuration()
|
||||
val dur = vm.episode.media!!.getDuration()
|
||||
val durText = DurationConverter.getDurationStringLong(dur)
|
||||
Row {
|
||||
if (episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
if (vm.episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
if (farvoriteState)
|
||||
if (vm.farvoriteState)
|
||||
Icon(painter = painterResource(R.drawable.ic_star), tint = textColor, contentDescription = "isFavorite", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
if (inQueueState)
|
||||
if (vm.inQueueState)
|
||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val curContext = LocalContext.current
|
||||
val dateSizeText = " · " + formatAbbrev(curContext, episode.getPubDate()) + " · " + durText + " · " + if((episode.media?.size?:0) > 0) Formatter.formatShortFileSize(curContext, episode.media!!.size) else ""
|
||||
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " + if((vm.episode.media?.size?:0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media!!.size) else ""
|
||||
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
if (InTheatre.isCurMedia(episode.media) || inProgressState) {
|
||||
val pos = positionState
|
||||
val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||
Text(vm.episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
if (InTheatre.isCurMedia(vm.episode.media) || vm.inProgressState) {
|
||||
val pos = vm.positionState
|
||||
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||
Logd(TAG, "$index vm.prog: ${vm.prog}")
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(pos), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
LinearProgressIndicator(progress = { prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
||||
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
||||
Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
var actionButton by remember { mutableStateOf(if (actionButton_ == null) EpisodeActionButton.forItem(episodes[index]) else actionButton_(episodes[index])) }
|
||||
var actionRes by mutableIntStateOf(actionButton.getDrawable())
|
||||
var showAltActionsDialog by remember { mutableStateOf(false) }
|
||||
val dls = remember { DownloadServiceInterface.get() }
|
||||
var dlPercent by remember { mutableIntStateOf(0) }
|
||||
fun isDownloading(): Boolean {
|
||||
return episodes[index].downloadState.value > DownloadStatus.State.UNKNOWN.ordinal && episodes[index].downloadState.value < DownloadStatus.State.COMPLETED.ordinal
|
||||
return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
if (actionButton_ == null) {
|
||||
LaunchedEffect(episodes[index].downloadState.value) {
|
||||
if (index>=episodes.size) return@LaunchedEffect
|
||||
if (isDownloading()) dlPercent = dls?.getProgress(episodes[index].media!!.downloadUrl!!) ?: 0
|
||||
// Logd(TAG, "downloadState: ${episodes[index].downloadState.value} ${episode.media?.downloaded} $dlPercent")
|
||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
||||
actionRes = actionButton.getDrawable()
|
||||
LaunchedEffect(vms[index].downloadState) {
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media!!.downloadUrl!!) ?: 0
|
||||
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
|
||||
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
|
||||
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||
}
|
||||
LaunchedEffect(key1 = status) {
|
||||
if (index>=episodes.size) return@LaunchedEffect
|
||||
// Logd(TAG, "$index isPlayingState: ${episodes[index].isPlayingState.value} ${episodes[index].title}")
|
||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
||||
actionRes = actionButton.getDrawable()
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}")
|
||||
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
|
||||
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||
}
|
||||
// LaunchedEffect(vm.isPlayingState) {
|
||||
// Logd(TAG, "LaunchedEffect isPlayingState: $index ${vms[index].isPlayingState} ${vm.isPlayingState}")
|
||||
// vms[index].actionButton = EpisodeActionButton.forItem(vms[index].episode)
|
||||
// vms[index].actionRes = vm.actionButton.getDrawable()
|
||||
// }
|
||||
}
|
||||
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically).pointerInput(Unit) {
|
||||
detectTapGestures(onLongPress = { showAltActionsDialog = true }, onTap = {
|
||||
actionButton.onClick(activity)
|
||||
detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = {
|
||||
vm.actionButton?.onClick(activity)
|
||||
})
|
||||
}, contentAlignment = Alignment.Center) {
|
||||
// actionRes = actionButton.getDrawable()
|
||||
Icon(painter = painterResource(actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
||||
if (isDownloading() && dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * dlPercent}, strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
|
||||
Icon(painter = painterResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
||||
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent}, strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
|
||||
}
|
||||
if (showAltActionsDialog) actionButton.AltActionsDialog(activity, showAltActionsDialog, onDismiss = { showAltActionsDialog = false })
|
||||
if (vm.showAltActionsDialog) vm.actionButton?.AltActionsDialog(activity, vm.showAltActionsDialog, onDismiss = { vm.showAltActionsDialog = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +493,7 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
.clickable(onClick = {
|
||||
selected.clear()
|
||||
for (i in 0..longPressIndex) {
|
||||
selected.add(episodes[i])
|
||||
selected.add(vms[i].episode)
|
||||
}
|
||||
selectedSize = selected.size
|
||||
Logd(TAG, "selectedIds: ${selected.size}")
|
||||
|
@ -459,8 +501,8 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
Icon(painter = painterResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
|
||||
.clickable(onClick = {
|
||||
selected.clear()
|
||||
for (i in longPressIndex..<episodes.size) {
|
||||
selected.add(episodes[i])
|
||||
for (i in longPressIndex..<vms.size) {
|
||||
selected.add(vms[i].episode)
|
||||
}
|
||||
selectedSize = selected.size
|
||||
Logd(TAG, "selectedIds: ${selected.size}")
|
||||
|
@ -468,12 +510,11 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
var selectAllRes by remember { mutableIntStateOf(R.drawable.ic_select_all) }
|
||||
Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
|
||||
.clickable(onClick = {
|
||||
if (selectedSize != episodes.size) {
|
||||
if (selectedSize != vms.size) {
|
||||
selected.clear()
|
||||
selected.addAll(episodes)
|
||||
// for (e in episodes) {
|
||||
// selected.add(e)
|
||||
// }
|
||||
for (vm in vms) {
|
||||
selected.add(vm.episode)
|
||||
}
|
||||
selectAllRes = R.drawable.ic_select_none
|
||||
} else {
|
||||
selected.clear()
|
||||
|
@ -488,3 +529,43 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun confirmAddYoutubeEpisode(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) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), ) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.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())
|
||||
}
|
||||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
for (url in sharedUrls) {
|
||||
val info = StreamInfo.getInfo(Vista.getService(0), url)
|
||||
val episode = episodeFromStreamInfo(info)
|
||||
addToYoutubeSyndicate(episode, !audioOnly)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
toastMassege = "Receive share error: ${e.message}"
|
||||
Log.e(TAG, toastMassege)
|
||||
showToast = true
|
||||
}
|
||||
}
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,24 +4,26 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.net.feed.FeedBuilder
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -35,16 +37,8 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
|
|||
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(align = Alignment.Center)
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
Text("Subscribe: \"${feed.title}\" ?")
|
||||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
@ -73,15 +67,36 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
|
|||
Column(Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp).combinedClickable(
|
||||
onClick = {
|
||||
if (feed.feedUrl != null) {
|
||||
val fragment = OnlineFeedFragment.newInstance(feed.feedUrl)
|
||||
fragment.feedSource = feed.source
|
||||
activity.loadChildFragment(fragment)
|
||||
if (feed.feedId > 0) {
|
||||
val fragment = FeedEpisodesFragment.newInstance(feed.feedId)
|
||||
activity.loadChildFragment(fragment)
|
||||
} else {
|
||||
val fragment = OnlineFeedFragment.newInstance(feed.feedUrl)
|
||||
fragment.feedSource = feed.source
|
||||
activity.loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
}, onLongClick = { showSubscribeDialog.value = true })) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
|
||||
Row {
|
||||
AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), modifier = Modifier.width(65.dp).height(65.dp))
|
||||
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||
val (imgvCover, checkMark) = createRefs()
|
||||
AsyncImage(model = feed.imageUrl,
|
||||
contentDescription = "imgvCover",
|
||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
})
|
||||
val alpha = if (feed.feedId > 0) 1.0f else 0f
|
||||
if (feed.feedId > 0) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
})
|
||||
}
|
||||
Column(Modifier.padding(start = 10.dp)) {
|
||||
var authorText by remember { mutableStateOf("") }
|
||||
authorText = when {
|
||||
|
|
|
@ -14,11 +14,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.getPrefsWithDefaults
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.getSharedPrefs
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.isSwipeActionEnabled
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.getPrefsWithDefaults
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.getSharedPrefs
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.isSwipeActionEnabled
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
|
|
@ -32,7 +32,7 @@ import ac.mdiq.podcini.storage.utils.ChapterUtils
|
|||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||
|
@ -61,7 +61,6 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
|
@ -3,16 +3,13 @@ package ac.mdiq.podcini.ui.fragment
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus.State.UNKNOWN
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.InforBar
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
|
@ -21,6 +18,7 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.util.Pair
|
||||
|
@ -54,7 +52,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
lateinit var toolbar: MaterialToolbar
|
||||
lateinit var swipeActions: SwipeActions
|
||||
|
||||
val episodes = mutableStateListOf<Episode>()
|
||||
val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
@ -79,17 +78,22 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
|
||||
swipeActions = SwipeActions(this, TAG)
|
||||
lifecycle.addObserver(swipeActions)
|
||||
binding.infobar.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
}
|
||||
}
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
|
||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
Column {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
EpisodeLazyColumn(
|
||||
activity as MainActivity, vms = vms,
|
||||
leftSwipeCB = {
|
||||
if (leftActionState.value == null) swipeActions.showDialog()
|
||||
else leftActionState.value?.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
rightSwipeCB = {
|
||||
if (rightActionState.value == null) swipeActions.showDialog()
|
||||
else rightActionState.value?.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,6 +147,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
episodes.clear()
|
||||
vms.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -160,8 +165,10 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
for (url in event.urls) {
|
||||
// if (!event.isCompleted(url)) continue
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, url)
|
||||
if (pos >= 0) episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,10 +227,12 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
val data = withContext(Dispatchers.IO) {
|
||||
Pair(loadData().toMutableList(), loadTotalItemCount())
|
||||
}
|
||||
val restoreScrollPosition = episodes.isEmpty()
|
||||
episodes.clear()
|
||||
episodes.addAll(data.first)
|
||||
withContext(Dispatchers.Main) {
|
||||
val restoreScrollPosition = episodes.isEmpty()
|
||||
episodes.clear()
|
||||
episodes.addAll(data.first)
|
||||
vms.clear()
|
||||
for (e in data.first) { vms.add(EpisodeVM(e)) }
|
||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
|
||||
updateToolbar()
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.DownloadResultComparator
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.ui.actions.DownloadActionButton
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
|
|
|
@ -15,14 +15,11 @@ import ac.mdiq.podcini.storage.model.EpisodeFilter
|
|||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
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.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.InforBar
|
||||
import ac.mdiq.podcini.ui.compose.queueChanged
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
|
||||
|
@ -38,6 +35,7 @@ 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.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -61,7 +59,8 @@ import java.util.*
|
|||
private val binding get() = _binding!!
|
||||
|
||||
private var runningDownloads: Set<String> = HashSet()
|
||||
private val episodes = mutableStateListOf<Episode>()
|
||||
private val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var infoBarText = mutableStateOf("")
|
||||
private var leftActionState = mutableStateOf<SwipeAction?>(null)
|
||||
|
@ -93,18 +92,21 @@ import java.util.*
|
|||
|
||||
swipeActions = SwipeActions(this, TAG)
|
||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||
binding.infobar.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
}
|
||||
}
|
||||
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
|
||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
actionButton_ = { DeleteActionButton(it) } )
|
||||
Column {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms,
|
||||
leftSwipeCB = {
|
||||
if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(
|
||||
it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
rightSwipeCB = {
|
||||
if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(
|
||||
it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
actionButton_ = { DeleteActionButton(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||
|
@ -143,11 +145,10 @@ import java.util.*
|
|||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
// adapter.endSelectMode()
|
||||
// adapter.clearData()
|
||||
toolbar.setOnMenuItemClickListener(null)
|
||||
toolbar.setOnLongClickListener(null)
|
||||
episodes.clear()
|
||||
vms.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -295,8 +296,12 @@ import java.util.*
|
|||
val pos = EpisodeUtil.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)
|
||||
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
|
||||
|
@ -313,8 +318,12 @@ import java.util.*
|
|||
val pos = EpisodeUtil.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)
|
||||
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
|
||||
|
@ -330,6 +339,7 @@ import java.util.*
|
|||
private var loadItemsRunning = false
|
||||
private fun loadItems() {
|
||||
emptyView.hide()
|
||||
Logd(TAG, "loadItems() called")
|
||||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
|
@ -353,6 +363,10 @@ import java.util.*
|
|||
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))
|
||||
|
@ -364,13 +378,13 @@ import java.util.*
|
|||
private fun getEpisdesWithUrl(urls: List<String>): List<Episode> {
|
||||
Logd(TAG, "getEpisdesWithUrl() called ")
|
||||
if (urls.isEmpty()) return listOf()
|
||||
val episodes: MutableList<Episode> = mutableListOf()
|
||||
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_)
|
||||
if (item_ != null) episodes_.add(item_)
|
||||
}
|
||||
return realm.copyFromRealm(episodes)
|
||||
return realm.copyFromRealm(episodes_)
|
||||
}
|
||||
|
||||
private fun refreshInfoBar() {
|
||||
|
@ -380,7 +394,7 @@ import java.util.*
|
|||
for (item in episodes) sizeMB += item.media?.size ?: 0
|
||||
info += " • " + (sizeMB / 1000000) + " MB"
|
||||
}
|
||||
Logd(TAG, "filter value: ${getFilter().values.size} ${getFilter().values.joinToString()}")
|
||||
Logd(TAG, "refreshInfoBar filter value: ${getFilter().values.size} ${getFilter().values.joinToString()}")
|
||||
if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}"
|
||||
infoBarText.value = info
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import ac.mdiq.podcini.storage.model.EpisodeMedia
|
|||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.*
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.*
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
|
@ -61,7 +61,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
|
|
@ -17,8 +17,8 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
|||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
|
@ -79,7 +79,10 @@ import java.util.concurrent.Semaphore
|
|||
private var headerCreated = false
|
||||
private var feedID: Long = 0
|
||||
private var feed by mutableStateOf<Feed?>(null)
|
||||
private val episodes = mutableStateListOf<Episode>()
|
||||
|
||||
private val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var ieMap: Map<Long, Int> = mapOf()
|
||||
private var ueMap: Map<String, Int> = mapOf()
|
||||
|
||||
|
@ -143,6 +146,8 @@ import java.util.concurrent.Semaphore
|
|||
episodes.addAll(etmp)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
vms.clear()
|
||||
for (e in etmp) { vms.add(EpisodeVM(e)) }
|
||||
loadItemsRunning = false
|
||||
}
|
||||
}
|
||||
|
@ -151,16 +156,22 @@ import java.util.concurrent.Semaphore
|
|||
FeedEpisodesHeader(activity = (activity as MainActivity), feed = feed, filterButColor = filterButColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
|
||||
}
|
||||
}
|
||||
binding.infobar.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
}
|
||||
}
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes, refreshCB = {FeedUpdateManager.runOnceOrAsk(requireContext(), feed)},
|
||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}, )
|
||||
Column {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms,
|
||||
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
|
||||
leftSwipeCB = {
|
||||
if (leftActionState.value == null) swipeActions.showDialog()
|
||||
else leftActionState.value?.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
rightSwipeCB = {
|
||||
if (rightActionState.value == null) swipeActions.showDialog()
|
||||
else rightActionState.value?.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,6 +295,7 @@ import java.util.concurrent.Semaphore
|
|||
ieMap = mapOf()
|
||||
ueMap = mapOf()
|
||||
episodes.clear()
|
||||
vms.clear()
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
ttsWorking = false
|
||||
|
@ -384,14 +396,12 @@ import java.util.concurrent.Semaphore
|
|||
|
||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||
// Logd(TAG, "onPlayEvent ${event.episode.title}")
|
||||
// if (feed != null) {
|
||||
// val pos: Int = ieMap[event.episode.id] ?: -1
|
||||
// if (pos >= 0) {
|
||||
//// if (!filterOutEpisode(event.episode)) episodes[pos].isPlayingState.value = event.isPlaying()
|
||||
//// if (filterOutEpisode(event.episode)) adapter.updateItems(episodes)
|
||||
//// else adapter.notifyItemChangedCompat(pos)
|
||||
// }
|
||||
// }
|
||||
if (feed != null) {
|
||||
val pos: Int = ieMap[event.episode.id] ?: -1
|
||||
if (pos >= 0) {
|
||||
if (!filterOutEpisode(event.episode)) vms[pos].isPlayingState = event.isPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
|
@ -403,8 +413,8 @@ import java.util.concurrent.Semaphore
|
|||
val pos: Int = ueMap[url] ?: -1
|
||||
if (pos >= 0) {
|
||||
Logd(TAG, "onEpisodeDownloadEvent $pos ${event.map[url]?.state} ${episodes[pos].media?.downloaded} ${episodes[pos].title}")
|
||||
episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
// adapter.notifyItemChangedCompat(pos)
|
||||
// episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -533,15 +543,15 @@ import java.util.concurrent.Semaphore
|
|||
while (loadItemsRunning) Thread.sleep(50)
|
||||
}
|
||||
|
||||
// private fun filterOutEpisode(episode: Episode): Boolean {
|
||||
// if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty() && !feed!!.episodeFilter.matches(episode)) {
|
||||
// episodes.remove(episode)
|
||||
// ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
// ueMap = episodes.mapIndexedNotNull { index, episode_ -> episode_.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
private fun filterOutEpisode(episode: Episode): Boolean {
|
||||
if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty() && !feed!!.episodeFilter.matches(episode)) {
|
||||
episodes.remove(episode)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode_ -> episode_.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// private fun redoFilter(list: List<Episode>? = null) {
|
||||
// if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty()) {
|
||||
|
@ -574,6 +584,10 @@ import java.util.concurrent.Semaphore
|
|||
episodes.addAll(etmp)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
withContext(Dispatchers.Main) {
|
||||
vms.clear()
|
||||
for (e in etmp) { vms.add(EpisodeVM(e)) }
|
||||
}
|
||||
if (onInit) {
|
||||
var hasNonMediaItems = false
|
||||
for (item in episodes) {
|
||||
|
|
|
@ -12,10 +12,9 @@ import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
|||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -23,7 +22,6 @@ import android.app.Dialog
|
|||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.LightingColorFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
|
@ -34,15 +32,24 @@ import android.view.LayoutInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.load
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.*
|
||||
|
@ -72,6 +79,13 @@ class OnlineFeedFragment : Fragment() {
|
|||
private var feedUrl: String = ""
|
||||
private lateinit var feedBuilder: FeedBuilder
|
||||
|
||||
private var showFeedDisplay by mutableStateOf(false)
|
||||
private var showProgress by mutableStateOf(true)
|
||||
private var autoDownloadChecked by mutableStateOf(false)
|
||||
private var enableSubscribe by mutableStateOf(true)
|
||||
private var enableEpisodes by mutableStateOf(true)
|
||||
private var subButTextRes by mutableIntStateOf(R.string.subscribing_label)
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
if (feeds == null) return 0
|
||||
|
@ -83,6 +97,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
|
||||
@Volatile
|
||||
private var feeds: List<Feed>? = null
|
||||
private var feed by mutableStateOf<Feed?>(null)
|
||||
private var selectedDownloadUrl: String? = null
|
||||
// private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
|
@ -97,25 +112,25 @@ class OnlineFeedFragment : Fragment() {
|
|||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
||||
binding.closeButton.visibility = View.INVISIBLE
|
||||
binding.card.setOnClickListener(null)
|
||||
binding.card.setCardBackgroundColor(getColorFromAttr(requireContext(), com.google.android.material.R.attr.colorSurface))
|
||||
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
||||
Logd(TAG, "feedUrl: $feedUrl")
|
||||
|
||||
feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) }
|
||||
|
||||
binding.mainView.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
MainView()
|
||||
}
|
||||
}
|
||||
if (feedUrl.isEmpty()) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
} else {
|
||||
Logd(TAG, "Activity was started with url $feedUrl")
|
||||
setLoadingLayout()
|
||||
showProgress = true
|
||||
// Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL
|
||||
if (feedUrl.contains("subscribeonandroid.com")) feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "")
|
||||
if (savedInstanceState != null) {
|
||||
|
@ -127,11 +142,6 @@ class OnlineFeedFragment : Fragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
private fun setLoadingLayout() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.feedDisplayContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isPaused = false
|
||||
|
@ -164,9 +174,10 @@ class OnlineFeedFragment : Fragment() {
|
|||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
feedBuilder.startFeedBuilding(urlString, username, password) { feed, map ->
|
||||
feedBuilder.startFeedBuilding(urlString, username, password) { feed_, map ->
|
||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||
showFeedInformation(feed, map)
|
||||
feed = feed_
|
||||
showFeedInformation(feed_, map)
|
||||
}
|
||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||
} catch (e: Throwable) {
|
||||
|
@ -196,9 +207,10 @@ class OnlineFeedFragment : Fragment() {
|
|||
Logd(TAG, "Successfully retrieve feed url")
|
||||
isFeedFoundBySearch = true
|
||||
feeds = getFeedList()
|
||||
feedBuilder.startFeedBuilding(url, username, password) { feed, map ->
|
||||
feedBuilder.startFeedBuilding(url, username, password) { feed_, map ->
|
||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||
showFeedInformation(feed, map)
|
||||
feed = feed_
|
||||
showFeedInformation(feed_, map)
|
||||
}
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
|
@ -268,77 +280,140 @@ class OnlineFeedFragment : Fragment() {
|
|||
* This method is executed on the GUI thread.
|
||||
*/
|
||||
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.feedDisplayContainer.visibility = View.VISIBLE
|
||||
showProgress = false
|
||||
// binding.feedDisplayContainer.visibility = View.VISIBLE
|
||||
showFeedDisplay = true
|
||||
if (isFeedFoundBySearch) {
|
||||
val resId = R.string.no_feed_url_podcast_found_by_search
|
||||
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
binding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
||||
binding.episodeLabel.setOnClickListener { showEpisodes(feed.episodes)}
|
||||
if (!feed.imageUrl.isNullOrBlank()) {
|
||||
binding.coverImage.load(feed.imageUrl) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
}
|
||||
}
|
||||
binding.titleLabel.text = feed.title
|
||||
binding.authorLabel.text = feed.author
|
||||
binding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
|
||||
binding.txtvTechInfo.text = "${feed.episodes.size} episodes\n" +
|
||||
"${feed.mostRecentItem?.title ?: ""}\n\n" +
|
||||
"${feed.language} ${feed.type ?: ""} ${feed.lastUpdate ?: ""}\n" +
|
||||
"${feed.link}\n" +
|
||||
"${feed.downloadUrl}"
|
||||
binding.subscribeButton.setOnClickListener {
|
||||
if (feedInFeedlist()) openFeed()
|
||||
else {
|
||||
lifecycleScope.launch {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
withContext(Dispatchers.IO) { feedBuilder.subscribe(feed) }
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
didPressSubscribe = true
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (feedSource != "VistaGuide" && isEnableAutodownload)
|
||||
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||
|
||||
if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
||||
else {
|
||||
binding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||
val alternateUrlsList: MutableList<String> = ArrayList()
|
||||
val alternateUrlsTitleList: MutableList<String?> = ArrayList()
|
||||
if (feed.downloadUrl != null) alternateUrlsList.add(feed.downloadUrl!!)
|
||||
alternateUrlsTitleList.add(feed.title)
|
||||
alternateUrlsList.addAll(alternateFeedUrls.keys)
|
||||
for (url in alternateFeedUrls.keys) {
|
||||
alternateUrlsTitleList.add(alternateFeedUrls[url])
|
||||
}
|
||||
val adapter: ArrayAdapter<String> = object : ArrayAdapter<String>(requireContext(),
|
||||
R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
// reusing the old view causes a visual bug on Android <= 10
|
||||
return super.getDropDownView(position, null, parent)
|
||||
}
|
||||
}
|
||||
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
||||
binding.alternateUrlsSpinner.adapter = adapter
|
||||
binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
selectedDownloadUrl = alternateUrlsList[position]
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
}
|
||||
// if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
||||
// else {
|
||||
// binding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||
// val alternateUrlsList: MutableList<String> = ArrayList()
|
||||
// val alternateUrlsTitleList: MutableList<String?> = ArrayList()
|
||||
// if (feed.downloadUrl != null) alternateUrlsList.add(feed.downloadUrl!!)
|
||||
// alternateUrlsTitleList.add(feed.title)
|
||||
// alternateUrlsList.addAll(alternateFeedUrls.keys)
|
||||
// for (url in alternateFeedUrls.keys) {
|
||||
// alternateUrlsTitleList.add(alternateFeedUrls[url])
|
||||
// }
|
||||
// val adapter: ArrayAdapter<String> = object : ArrayAdapter<String>(requireContext(),
|
||||
// R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
||||
// override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
// // reusing the old view causes a visual bug on Android <= 10
|
||||
// return super.getDropDownView(position, null, parent)
|
||||
// }
|
||||
// }
|
||||
// adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
||||
// binding.alternateUrlsSpinner.adapter = adapter
|
||||
// binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
// override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
// selectedDownloadUrl = alternateUrlsList[position]
|
||||
// }
|
||||
// override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
// }
|
||||
// }
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
@UnstableApi private fun openFeed() {
|
||||
(activity as MainActivity).loadFeedFragmentById(feedId, null)
|
||||
@Composable
|
||||
fun MainView() {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
|
||||
val (progressBar, main) = createRefs()
|
||||
if (showProgress) CircularProgressIndicator(progress = { 0.6f },
|
||||
strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) })
|
||||
else Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp)
|
||||
.constrainAs(main) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
}) {
|
||||
if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) {
|
||||
val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs()
|
||||
if (false) Image(painter = painterResource(R.drawable.ic_settings_white), contentDescription = "background",
|
||||
Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
})
|
||||
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage",
|
||||
Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
}.clickable(onClick = {}))
|
||||
Column(Modifier.constrainAs(taColumn) {
|
||||
top.linkTo(coverImage.top)
|
||||
start.linkTo(coverImage.end) }) {
|
||||
Text(feed?.title?:"No title", color = textColor, style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
Text(feed?.author?:"", color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row(Modifier.constrainAs(buttons) {
|
||||
start.linkTo(coverImage.end)
|
||||
top.linkTo(taColumn.bottom)
|
||||
end.linkTo(parent.end)
|
||||
}) {
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
if (enableSubscribe) Button(onClick = {
|
||||
if (feedInFeedlist()) (activity as MainActivity).loadFeedFragmentById(feedId, null)
|
||||
else {
|
||||
enableSubscribe = false
|
||||
enableEpisodes = false
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
feedBuilder.subscribe(feed!!)
|
||||
withContext(Dispatchers.Main) {
|
||||
enableSubscribe = true
|
||||
didPressSubscribe = true
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(subButTextRes))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) {
|
||||
Text(stringResource(R.string.episodes_label))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
}
|
||||
if (false) Icon(painter = painterResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier
|
||||
.constrainAs(closeButton) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
})
|
||||
}
|
||||
Column {
|
||||
// alternate_urls_spinner
|
||||
if (feedSource != "VistaGuide" && isEnableAutodownload) Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = autoDownloadChecked, onCheckedChange = { autoDownloadChecked = it })
|
||||
Text(text = stringResource(R.string.auto_download_label),
|
||||
style = MaterialTheme.typography.bodyMedium.merge(), color = textColor,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
val scrollState = rememberScrollState()
|
||||
var numEpisodes by remember { mutableIntStateOf(feed?.episodes?.size?:0) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
numEpisodes = feed?.episodes?.size?:0
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).verticalScroll(scrollState)) {
|
||||
Text("$numEpisodes episodes", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 10.dp))
|
||||
Text(stringResource(R.string.description_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp))
|
||||
Text(HtmlToPlainText.getPlainText(feed?.description?:""), color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(feed?.mostRecentItem?.title ?: "", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp))
|
||||
Text("${feed?.language?:""} ${feed?.type ?: ""} ${feed?.lastUpdate ?: ""}", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp))
|
||||
Text(feed?.link?:"", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp))
|
||||
Text(feed?.downloadUrl?:"", color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun showEpisodes(episodes: MutableList<Episode>) {
|
||||
|
@ -364,12 +439,12 @@ class OnlineFeedFragment : Fragment() {
|
|||
// binding.subscribeButton.isEnabled = false
|
||||
// }
|
||||
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
|
||||
binding.subscribeButton.isEnabled = false
|
||||
binding.subscribeButton.setText(R.string.subscribing_label)
|
||||
enableSubscribe = false
|
||||
subButTextRes = R.string.subscribing_label
|
||||
}
|
||||
feedInFeedlist() -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.open)
|
||||
enableSubscribe = true
|
||||
subButTextRes = R.string.open
|
||||
if (didPressSubscribe) {
|
||||
didPressSubscribe = false
|
||||
val feed1 = getFeed(feedId, true)?: return
|
||||
|
@ -379,7 +454,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
feed1.preferences!!.prefStreamOverDownload = true
|
||||
feed1.preferences!!.autoDownload = false
|
||||
} else if (isEnableAutodownload) {
|
||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
||||
val autoDownload = autoDownloadChecked
|
||||
feed1.preferences!!.autoDownload = autoDownload
|
||||
val editor = prefs!!.edit()
|
||||
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
||||
|
@ -390,13 +465,12 @@ class OnlineFeedFragment : Fragment() {
|
|||
feed1.preferences!!.password = password
|
||||
}
|
||||
persistFeedPreferences(feed1)
|
||||
openFeed()
|
||||
// openFeed()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.subscribe_label)
|
||||
if (feedSource != "VistaGuide" && isEnableAutodownload) binding.autoDownloadCheckBox.visibility = View.VISIBLE
|
||||
enableSubscribe = true
|
||||
subButTextRes = R.string.subscribing_label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,13 +26,10 @@ import ac.mdiq.podcini.storage.model.PlayQueue
|
|||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.InforBar
|
||||
import ac.mdiq.podcini.ui.compose.queueChanged
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
|
@ -77,7 +74,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
@ -113,7 +109,8 @@ import kotlin.math.max
|
|||
private lateinit var queues: List<PlayQueue>
|
||||
|
||||
private var displayUpArrow = false
|
||||
private val queueItems = mutableStateListOf<Episode>()
|
||||
private val queueItems = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var showBin by mutableStateOf(false)
|
||||
|
||||
|
@ -183,7 +180,7 @@ import kotlin.math.max
|
|||
|
||||
swipeActions = SwipeActions(this, TAG)
|
||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
||||
swipeActionsBin = SwipeActions(this, TAG)
|
||||
swipeActionsBin = SwipeActions(this, TAG+".Bin")
|
||||
swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
||||
|
||||
binding.lazyColumn.setContent {
|
||||
|
@ -199,7 +196,7 @@ import kotlin.math.max
|
|||
if (rightActionStateBin.value == null) swipeActionsBin.showDialog()
|
||||
else rightActionStateBin.value?.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
|
||||
}
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = queueItems, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
|
||||
}
|
||||
} else {
|
||||
Column {
|
||||
|
@ -212,7 +209,7 @@ import kotlin.math.max
|
|||
if (rightActionState.value == null) swipeActions.showDialog()
|
||||
else rightActionState.value?.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
|
||||
}
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = queueItems, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,11 +314,16 @@ import kotlin.math.max
|
|||
if (showBin) return
|
||||
when (event.action) {
|
||||
FlowEvent.QueueEvent.Action.ADDED -> {
|
||||
if (event.episodes.isNotEmpty() && !curQueue.contains(event.episodes[0])) queueItems.addAll(event.episodes)
|
||||
if (event.episodes.isNotEmpty() && !curQueue.contains(event.episodes[0])) {
|
||||
queueItems.addAll(event.episodes)
|
||||
for (e in event.episodes) vms.add(EpisodeVM(e))
|
||||
}
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> {
|
||||
queueItems.clear()
|
||||
queueItems.addAll(event.episodes)
|
||||
vms.clear()
|
||||
for (e in event.episodes) vms.add(EpisodeVM(e))
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
|
||||
if (event.episodes.isNotEmpty()) {
|
||||
|
@ -329,8 +331,10 @@ import kotlin.math.max
|
|||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
|
||||
if (pos >= 0) {
|
||||
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
|
||||
queueItems[pos].stopMonitoring.value = true
|
||||
// queueItems[pos].stopMonitoring.value = true
|
||||
queueItems.removeAt(pos)
|
||||
vms[pos].stopMonitoring()
|
||||
vms.removeAt(pos)
|
||||
} else {
|
||||
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
|
||||
continue
|
||||
|
@ -344,6 +348,7 @@ import kotlin.math.max
|
|||
}
|
||||
FlowEvent.QueueEvent.Action.CLEARED -> {
|
||||
queueItems.clear()
|
||||
vms.clear()
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return
|
||||
}
|
||||
|
@ -357,7 +362,7 @@ import kotlin.math.max
|
|||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
|
||||
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
|
||||
// if (pos >= 0) queueItems[pos].isPlayingState.value = event.isPlaying()
|
||||
if (pos >= 0) vms[pos].isPlayingState = event.isPlaying()
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
|
@ -366,7 +371,10 @@ import kotlin.math.max
|
|||
for (url in event.urls) {
|
||||
// if (!event.isCompleted(url)) continue
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url)
|
||||
if (pos >= 0) queueItems[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
if (pos >= 0) {
|
||||
// queueItems[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -417,6 +425,7 @@ import kotlin.math.max
|
|||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
queueItems.clear()
|
||||
vms.clear()
|
||||
toolbar.setOnMenuItemClickListener(null)
|
||||
toolbar.setOnLongClickListener(null)
|
||||
super.onDestroyView()
|
||||
|
@ -662,12 +671,14 @@ import kotlin.math.max
|
|||
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
||||
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
|
||||
queueItems.clear()
|
||||
vms.clear()
|
||||
if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
|
||||
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
|
||||
else {
|
||||
curQueue.episodes.clear()
|
||||
queueItems.addAll(curQueue.episodes)
|
||||
}
|
||||
for (e in queueItems) vms.add(EpisodeVM(e))
|
||||
Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}")
|
||||
|
||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||
|
|
|
@ -10,18 +10,19 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
|||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.handler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.actions.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeVM
|
||||
import ac.mdiq.podcini.ui.dialog.CustomFeedNameDialog
|
||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.view.SquareImageView
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
@ -68,7 +69,8 @@ class SearchFragment : Fragment() {
|
|||
private lateinit var chip: Chip
|
||||
private lateinit var automaticSearchDebouncer: Handler
|
||||
|
||||
private val results = mutableStateListOf<Episode>()
|
||||
private val results = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var lastQueryChange: Long = 0
|
||||
private var isOtherViewInFoucus = false
|
||||
|
@ -86,7 +88,7 @@ class SearchFragment : Fragment() {
|
|||
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = results)
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,6 +139,7 @@ class SearchFragment : Fragment() {
|
|||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
results.clear()
|
||||
vms.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -229,7 +232,10 @@ class SearchFragment : Fragment() {
|
|||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
for (url in event.urls) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, url)
|
||||
if (pos >= 0) results[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
if (pos >= 0) {
|
||||
// results[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -252,6 +258,8 @@ class SearchFragment : Fragment() {
|
|||
val first_ = results_.first!!.toMutableList()
|
||||
results.clear()
|
||||
results.addAll(first_)
|
||||
vms.clear()
|
||||
for (e in first_) { vms.add(EpisodeVM(e)) }
|
||||
}
|
||||
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
||||
if (results_.second != null) adapterFeeds.updateData(results_.second!!)
|
||||
|
|
|
@ -5,11 +5,11 @@ import ac.mdiq.podcini.databinding.FragmentSearchResultsBinding
|
|||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
|
@ -20,8 +20,10 @@ import android.view.ViewGroup
|
|||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
|
@ -31,15 +33,12 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -168,8 +167,18 @@ class SearchResultsFragment : Fragment() {
|
|||
private fun search(query: String) {
|
||||
showOnlyProgressBar()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val feeds = getFeedList()
|
||||
fun feedId(r: PodcastSearchResult): Long {
|
||||
for (f in feeds) {
|
||||
if (f.downloadUrl == r.feedUrl) return f.id
|
||||
}
|
||||
return 0L
|
||||
}
|
||||
try {
|
||||
val result = searchProvider?.search(query) ?: listOf()
|
||||
for (r in result) {
|
||||
r.feedId = feedId(r)
|
||||
}
|
||||
searchResults.clear()
|
||||
searchResults.addAll(result)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
@ -847,17 +847,21 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Column(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${feed.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
if (!feed.isBuilding) {
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
}
|
||||
}, onLongClick = {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
if (!feed.isBuilding) {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
}
|
||||
Logd(TAG, "long clicked: ${feed.title}")
|
||||
})) {
|
||||
|
@ -914,25 +918,31 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
modifier = Modifier.width(80.dp).height(80.dp)
|
||||
.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
||||
if (!feed.isBuilding) {
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
||||
}
|
||||
})
|
||||
)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${feed.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
if (!feed.isBuilding) {
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
}
|
||||
}, onLongClick = {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selected.clear()
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
if (!feed.isBuilding) {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selected.clear()
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
}
|
||||
Logd(TAG, "long clicked: ${feed.title}")
|
||||
})) {
|
||||
|
@ -944,9 +954,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var feedSortInfo by remember { mutableStateOf(feed.sortInfo) }
|
||||
LaunchedEffect(feedSorted) {
|
||||
feedSortInfo = feed.sortInfo
|
||||
}
|
||||
LaunchedEffect(feedSorted) { feedSortInfo = feed.sortInfo }
|
||||
Text(feedSortInfo, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.view
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.ui.actions.handler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.actions.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.util.*
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||
<!-- android:id="@+id/infobar"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"/>-->
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||
<!-- android:id="@+id/infobar"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"/>-->
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
|
|
@ -25,10 +25,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||
<!-- android:id="@+id/infobar"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"/>-->
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
|
|
@ -25,232 +25,9 @@
|
|||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/transparentBackground"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/mainView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="8dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
style="?android:attr/progressBarStyle" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/feed_display_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/feeditemlist_header_height"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@color/feed_image_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backgroundImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_height="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:background="@drawable/bg_rounded_corners"
|
||||
android:clipToOutline="true"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textFontWeight="800"
|
||||
tools:text="Podcast title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/author_label"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/titleLabel"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Podcast author" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/author_label"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/subscribeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/subscribe_label" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/episodeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/episodes_label"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_close_white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/alternate_urls_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:dropDownWidth="match_parent"
|
||||
android:padding="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="@dimen/text_size_micro" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/autoDownloadCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:checked="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/auto_download_label"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/online_feed_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/description_label"
|
||||
style="@style/TextAppearance.Material3.TitleMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:ellipsize="end"
|
||||
android:lineHeight="20dp"
|
||||
style="@style/Podcini.TextView.ListItemBody"
|
||||
tools:text="@string/design_time_lorem_ipsum" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvTechInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:ellipsize="end"
|
||||
style="@style/Podcini.TextView.ListItemBody"
|
||||
tools:text="@string/design_time_lorem_ipsum" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
19
changelog.md
19
changelog.md
|
@ -1,3 +1,20 @@
|
|||
# 6.9.0
|
||||
|
||||
* re-worked Compose states handling for Episodes lists, might have issues related bugs
|
||||
* opening OnlineFeed of Youtube channel is made more responsive with more background processing on constructing episodes
|
||||
* you can subscribe at any time
|
||||
* if you open Episodes, you will see the episodes constructed at the moment
|
||||
* episodes limit for Youtube channel, playlist and YTMusicplaylist is now at 1000
|
||||
* in OnlineFeed view, after subscribe, the FeedEpisode view does not open automatically, presenting options to open it or return to the SearchResults view
|
||||
* in online SearchResults, if an item is already subscribed, a check mark appears on the cover image, and when clicked, FeedEpisodes view is opened.
|
||||
* added FlowEvent posting when adding shared youtube media or reserved online episodes
|
||||
* receiving shared contents from Youtube now should support hostnames youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, and youtu.be
|
||||
* when reserving episodes from a Youtube channel list, like receiving shared media from Youtube, you can choose for "audio only"
|
||||
* the reserved episodes will be added into synthetic podcast of either "Youtube Syndicate" or "Youtube Syndicate Audio" rather than "Misc Syndicate" for other types of episodes
|
||||
* fixed independence of swipe actions in Queues Bin
|
||||
* OnlineFeed is in Jetpack Compose
|
||||
* in SharedReceiver activity, added error notice for shared Youtube media
|
||||
|
||||
# 6.8.7
|
||||
|
||||
* clear history really clears it
|
||||
|
@ -27,7 +44,7 @@
|
|||
* migrated mostly the following view to Jetpack Compose:
|
||||
* Queues, AudioPlayer, Subscriptions, FeedInfo, EpisodeInfo, FeedEpisodes, AllEpisodes, History, Search, and OnlineFeed
|
||||
* to counter this nasty issue that Google can't fix over 2 years: ForegroundServiceStartNotAllowedException
|
||||
* for this and near future releases, target SDK is set to 30 (Android 12), though built with SDK 35 and tested on Android 14
|
||||
* for this and near future releases, target SDK is set to 30 (Android 11), though built with SDK 35 and tested on Android 14
|
||||
* supposedly notification will not disappear and play will not stop through a playlist
|
||||
* please voice any irregularities you may see
|
||||
* on episode lists, show duration on the top row
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
Version 6.9.0
|
||||
|
||||
* re-worked Compose states handling for Episodes lists, might have issues related bugs
|
||||
* opening OnlineFeed of Youtube channel is made more responsive with more background processing on constructing episodes
|
||||
* you can subscribe at any time
|
||||
* if you open Episodes, you will see the episodes constructed at the moment
|
||||
* episodes limit for Youtube channel, playlist and YTMusicplaylist is now at 1000
|
||||
* in OnlineFeed view, after subscribe, the FeedEpisode view does not open automatically, presenting options to open it or return to the SearchResults view
|
||||
* in online SearchResults, if an item is already subscribed, a check mark appears on the cover image, and when clicked, FeedEpisodes view is opened.
|
||||
* added FlowEvent posting when adding shared youtube media or reserved online episodes
|
||||
* receiving shared contents from Youtube now should support hostnames youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, and youtu.be
|
||||
* when reserving episodes from a Youtube channel list, like receiving shared media from Youtube, you can choose for "audio only"
|
||||
* the reserved episodes will be added into synthetic podcast of either "Youtube Syndicate" or "Youtube Syndicate Audio" rather than "Misc Syndicate" for other types of episodes
|
||||
* fixed independence of swipe actions in Queues Bin
|
||||
* OnlineFeed is in Jetpack Compose
|
||||
* in SharedReceiver activity, added error notice for shared Youtube media
|
Loading…
Reference in New Issue