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)
|
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.
|
#### 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.
|
#### 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.
|
#### 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.
|
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
|
* 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
|
* 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
|
* 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
|
### Instant (or Wifi) sync
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020264
|
versionCode 3020265
|
||||||
versionName "6.8.7"
|
versionName "6.9.0"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -25,10 +25,7 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.realm.kotlin.ext.realmListOf
|
import io.realm.kotlin.ext.realmListOf
|
||||||
import io.realm.kotlin.types.RealmList
|
import io.realm.kotlin.types.RealmList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -50,22 +47,28 @@ 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") }
|
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||||
selectedDownloadUrl = prepareUrl(url)
|
selectedDownloadUrl = prepareUrl(url)
|
||||||
val feed_ = Feed(selectedDownloadUrl, null)
|
val feed_ = Feed(selectedDownloadUrl, null)
|
||||||
|
feed_.isBuilding = true
|
||||||
feed_.id = Feed.newId()
|
feed_.id = Feed.newId()
|
||||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||||
feed_.hasVideoMedia = true
|
feed_.hasVideoMedia = true
|
||||||
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
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
|
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch
|
||||||
feed_.title = playlistInfo.name
|
feed_.title = playlistInfo.name
|
||||||
feed_.description = playlistInfo.description?.content ?: ""
|
feed_.description = playlistInfo.description?.content ?: ""
|
||||||
feed_.author = playlistInfo.uploaderName
|
feed_.author = playlistInfo.uploaderName
|
||||||
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||||
|
feed_.episodes = realmListOf()
|
||||||
var infoItems = playlistInfo.relatedItems
|
var infoItems = playlistInfo.relatedItems
|
||||||
var nextPage = playlistInfo.nextPage
|
var nextPage = playlistInfo.nextPage
|
||||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
while (infoItems.isNotEmpty()) {
|
while (infoItems.isNotEmpty()) {
|
||||||
|
eList.clear()
|
||||||
for (r in infoItems) {
|
for (r in infoItems) {
|
||||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||||
|
@ -74,7 +77,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
e.feedId = feed_.id
|
e.feedId = feed_.id
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
if (nextPage == null || eList.size > 500) break
|
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||||
try {
|
try {
|
||||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||||
nextPage = page.nextPage
|
nextPage = page.nextPage
|
||||||
|
@ -85,8 +88,10 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
feed_.episodes.addAll(eList)
|
||||||
|
}
|
||||||
|
feed_.isBuilding = false
|
||||||
}
|
}
|
||||||
feed_.episodes = eList
|
|
||||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||||
} else {
|
} else {
|
||||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
val channelInfo = ChannelInfo.getInfo(service, url)
|
||||||
|
@ -102,11 +107,14 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
feed_.description = channelInfo.description
|
feed_.description = channelInfo.description
|
||||||
feed_.author = channelInfo.parentChannelName
|
feed_.author = channelInfo.parentChannelName
|
||||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||||
|
feed_.episodes = realmListOf()
|
||||||
|
|
||||||
var infoItems = channelTabInfo.relatedItems
|
var infoItems = channelTabInfo.relatedItems
|
||||||
var nextPage = channelTabInfo.nextPage
|
var nextPage = channelTabInfo.nextPage
|
||||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
while (infoItems.isNotEmpty()) {
|
while (infoItems.isNotEmpty()) {
|
||||||
|
eList.clear()
|
||||||
for (r in infoItems) {
|
for (r in infoItems) {
|
||||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||||
|
@ -115,7 +123,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
e.feedId = feed_.id
|
e.feedId = feed_.id
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
if (nextPage == null || eList.size > 200) break
|
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||||
try {
|
try {
|
||||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||||
nextPage = page.nextPage
|
nextPage = page.nextPage
|
||||||
|
@ -126,8 +134,10 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
feed_.episodes.addAll(eList)
|
||||||
|
}
|
||||||
|
feed_.isBuilding = false
|
||||||
}
|
}
|
||||||
feed_.episodes = eList
|
|
||||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logd(TAG, "startFeedBuilding error1 ${e.message}")
|
Logd(TAG, "startFeedBuilding error1 ${e.message}")
|
||||||
|
@ -202,8 +212,11 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
val destinationFile = File(destination)
|
val destinationFile = File(destination)
|
||||||
return try {
|
return try {
|
||||||
val feed = Feed(selectedDownloadUrl, null)
|
val feed = Feed(selectedDownloadUrl, null)
|
||||||
|
feed.isBuilding = true
|
||||||
feed.fileUrl = destination
|
feed.fileUrl = destination
|
||||||
FeedHandler().parseFeed(feed)
|
val result = FeedHandler().parseFeed(feed)
|
||||||
|
feed.isBuilding = false
|
||||||
|
result
|
||||||
} catch (e: FeedHandler.UnsupportedFeedtypeException) {
|
} catch (e: FeedHandler.UnsupportedFeedtypeException) {
|
||||||
Logd(TAG, "Unsupported feed type detected")
|
Logd(TAG, "Unsupported feed type detected")
|
||||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
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) {
|
fun subscribe(feed: Feed) {
|
||||||
|
while (feed.isBuilding) {
|
||||||
|
runBlocking { delay(200) }
|
||||||
|
}
|
||||||
feed.id = 0L
|
feed.id = 0L
|
||||||
for (item in feed.episodes) {
|
for (item in feed.episodes) {
|
||||||
item.id = 0L
|
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.podcini.net.sync.gpoddernet.model.GpodnetPodcast
|
||||||
import ac.mdiq.vista.extractor.channel.ChannelInfoItem
|
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 de.mfietz.fyydlin.SearchHit
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
@ -18,6 +21,9 @@ class PodcastSearchResult private constructor(
|
||||||
val subscriberCount: Int,
|
val subscriberCount: Int,
|
||||||
val source: String) {
|
val source: String) {
|
||||||
|
|
||||||
|
// feedId will be positive if already subscribed
|
||||||
|
var feedId by mutableLongStateOf(0L)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun dummy(): PodcastSearchResult {
|
fun dummy(): PodcastSearchResult {
|
||||||
return PodcastSearchResult("", "", "", "", 0, "", -1, "dummy")
|
return PodcastSearchResult("", "", "", "", 0, "", -1, "dummy")
|
||||||
|
|
|
@ -25,7 +25,6 @@ class FeedHandler {
|
||||||
// val tg = TypeGetter()
|
// val tg = TypeGetter()
|
||||||
val type = getType(feed)
|
val type = getType(feed)
|
||||||
val handler = SyndHandler(feed, type)
|
val handler = SyndHandler(feed, type)
|
||||||
|
|
||||||
if (feed.fileUrl != null) {
|
if (feed.fileUrl != null) {
|
||||||
val factory = SAXParserFactory.newInstance()
|
val factory = SAXParserFactory.newInstance()
|
||||||
factory.isNamespaceAware = true
|
factory.isNamespaceAware = true
|
||||||
|
|
|
@ -436,6 +436,7 @@ object Feeds {
|
||||||
feed.preferences!!.queueId = -2L
|
feed.preferences!!.queueId = -2L
|
||||||
feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
||||||
upsertBlk(feed) {}
|
upsertBlk(feed) {}
|
||||||
|
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,6 +453,7 @@ object Feeds {
|
||||||
upsertBlk(episode) {}
|
upsertBlk(episode) {}
|
||||||
feed.episodes.add(episode)
|
feed.episodes.add(episode)
|
||||||
upsertBlk(feed) {}
|
upsertBlk(feed) {}
|
||||||
|
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMiscSyndicate(): Feed {
|
private fun getMiscSyndicate(): Feed {
|
||||||
|
@ -470,6 +472,7 @@ object Feeds {
|
||||||
feed.preferences!!.queueId = -2L
|
feed.preferences!!.queueId = -2L
|
||||||
// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
||||||
upsertBlk(feed) {}
|
upsertBlk(feed) {}
|
||||||
|
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,6 +488,7 @@ object Feeds {
|
||||||
upsertBlk(episode) {}
|
upsertBlk(episode) {}
|
||||||
feed.episodes.add(episode)
|
feed.episodes.add(episode)
|
||||||
upsertBlk(feed) {}
|
upsertBlk(feed) {}
|
||||||
|
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -135,15 +135,9 @@ class Episode : RealmObject {
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
|
||||||
val downloadState = mutableIntStateOf(if (media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
val isRemote = mutableStateOf(false)
|
val isRemote = mutableStateOf(false)
|
||||||
|
|
||||||
@Ignore
|
|
||||||
val stopMonitoring = mutableStateOf(false)
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.playState = PlayState.UNPLAYED.code
|
this.playState = PlayState.UNPLAYED.code
|
||||||
}
|
}
|
||||||
|
@ -162,13 +156,6 @@ class Episode : RealmObject {
|
||||||
this.feed = feed
|
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) {
|
fun updateFromOther(other: Episode) {
|
||||||
if (other.imageUrl != null) this.imageUrl = other.imageUrl
|
if (other.imageUrl != null) this.imageUrl = other.imageUrl
|
||||||
if (other.title != null) title = other.title
|
if (other.title != null) title = other.title
|
||||||
|
@ -296,10 +283,6 @@ class Episode : RealmObject {
|
||||||
if (isFavorite != other.isFavorite) return false
|
if (isFavorite != other.isFavorite) return false
|
||||||
if (isInProgress != other.isInProgress) return false
|
if (isInProgress != other.isInProgress) return false
|
||||||
if (isDownloaded != other.isDownloaded) 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
|
return true
|
||||||
}
|
}
|
||||||
|
@ -324,24 +307,9 @@ class Episode : RealmObject {
|
||||||
result = 31 * result + isFavorite.hashCode()
|
result = 31 * result + isFavorite.hashCode()
|
||||||
result = 31 * result + isInProgress.hashCode()
|
result = 31 * result + isInProgress.hashCode()
|
||||||
result = 31 * result + isDownloaded.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
|
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) {
|
enum class PlayState(val code: Int) {
|
||||||
UNSPECIFIED(-2),
|
UNSPECIFIED(-2),
|
||||||
NEW(-1),
|
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.FeedFunding.Companion.extractPaymentLinks
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
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.ext.realmListOf
|
||||||
import io.realm.kotlin.types.RealmList
|
import io.realm.kotlin.types.RealmList
|
||||||
import io.realm.kotlin.types.RealmObject
|
import io.realm.kotlin.types.RealmObject
|
||||||
|
@ -141,6 +144,9 @@ class Feed : RealmObject {
|
||||||
@Ignore
|
@Ignore
|
||||||
var sortInfo: String = ""
|
var sortInfo: String = ""
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
var isBuilding by mutableStateOf(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This constructor is used for test purposes.
|
* 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.R
|
||||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
|
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.R
|
||||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
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.Menu
|
||||||
import android.view.MenuItem
|
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 android.content.Context
|
||||||
import androidx.annotation.AttrRes
|
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.R
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
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.EpisodeFilter
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
import ac.mdiq.podcini.ui.actions.SwipeAction.Companion.NO_ACTION
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction.Companion.NO_ACTION
|
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog
|
import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog
|
||||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
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.buildTags
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.monitorFeeds
|
import ac.mdiq.podcini.storage.database.Feeds.monitorFeeds
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
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.activity.starter.MainActivityStarter
|
||||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||||
import ac.mdiq.podcini.ui.fragment.*
|
import ac.mdiq.podcini.ui.fragment.*
|
||||||
|
@ -65,7 +65,6 @@ import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package ac.mdiq.podcini.ui.activity
|
package ac.mdiq.podcini.ui.activity
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
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.CustomTheme
|
||||||
|
import ac.mdiq.podcini.ui.compose.confirmAddYoutubeEpisode
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.vista.extractor.Vista
|
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
|
||||||
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
|
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
|
||||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -16,20 +14,11 @@ import android.util.Log
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.runtime.remember
|
||||||
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.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import java.net.URL
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class ShareReceiverActivity : AppCompatActivity() {
|
class ShareReceiverActivity : AppCompatActivity() {
|
||||||
|
@ -54,6 +43,7 @@ class ShareReceiverActivity : AppCompatActivity() {
|
||||||
if (urlString != null) sharedUrl = URLDecoder.decode(urlString, "UTF-8")
|
if (urlString != null) sharedUrl = URLDecoder.decode(urlString, "UTF-8")
|
||||||
}
|
}
|
||||||
Logd(TAG, "feedUrl: $sharedUrl")
|
Logd(TAG, "feedUrl: $sharedUrl")
|
||||||
|
val url = URL(sharedUrl)
|
||||||
when {
|
when {
|
||||||
// plain text
|
// plain text
|
||||||
sharedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> {
|
sharedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> {
|
||||||
|
@ -62,12 +52,13 @@ class ShareReceiverActivity : AppCompatActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
// Youtube media
|
// 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")
|
Logd(TAG, "got youtube media")
|
||||||
setContent {
|
setContent {
|
||||||
val showDialog = remember { mutableStateOf(true) }
|
val showDialog = remember { mutableStateOf(true) }
|
||||||
CustomTheme(this@ShareReceiverActivity) {
|
CustomTheme(this@ShareReceiverActivity) {
|
||||||
confirmAddEpisode(showDialog.value, onDismissRequest = {
|
confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
finish()
|
finish()
|
||||||
})
|
})
|
||||||
|
@ -85,49 +76,44 @@ class ShareReceiverActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
// @Composable
|
||||||
fun confirmAddEpisode(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
// fun confirmAddEpisode(sharedUrl: String, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||||
if (showDialog) {
|
// var showToast by remember { mutableStateOf(false) }
|
||||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
// var toastMassege by remember { mutableStateOf("")}
|
||||||
Card(
|
// if (showToast) CustomToast(message = toastMassege, onDismiss = { showToast = false })
|
||||||
modifier = Modifier
|
//
|
||||||
.wrapContentSize(align = Alignment.Center)
|
// if (showDialog) {
|
||||||
.padding(16.dp),
|
// Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||||
shape = RoundedCornerShape(16.dp),
|
// Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||||
) {
|
// Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||||
Column(
|
// var audioOnly by remember { mutableStateOf(false) }
|
||||||
modifier = Modifier.padding(16.dp),
|
// Row(Modifier.fillMaxWidth()) {
|
||||||
verticalArrangement = Arrangement.Center
|
// Checkbox(checked = audioOnly, onCheckedChange = { audioOnly = it })
|
||||||
) {
|
// Text(text = stringResource(R.string.pref_video_mode_audio_only), style = MaterialTheme.typography.bodyLarge.merge())
|
||||||
var audioOnly by remember { mutableStateOf(false) }
|
// }
|
||||||
Row(Modifier.fillMaxWidth()) {
|
// Button(onClick = {
|
||||||
Checkbox(checked = audioOnly,
|
// CoroutineScope(Dispatchers.IO).launch {
|
||||||
onCheckedChange = {
|
// try {
|
||||||
audioOnly = it
|
// val info = StreamInfo.getInfo(Vista.getService(0), sharedUrl)
|
||||||
}
|
// Logd(TAG, "info: $info")
|
||||||
)
|
// val episode = episodeFromStreamInfo(info)
|
||||||
Text(
|
// Logd(TAG, "episode: $episode")
|
||||||
text = stringResource(R.string.pref_video_mode_audio_only),
|
// addToYoutubeSyndicate(episode, !audioOnly)
|
||||||
style = MaterialTheme.typography.bodyLarge.merge(),
|
// } catch (e: Throwable) {
|
||||||
)
|
// toastMassege = "Receive share error: ${e.message}"
|
||||||
}
|
// Log.e(TAG, toastMassege)
|
||||||
Button(onClick = {
|
// showToast = true
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
// }
|
||||||
val info = StreamInfo.getInfo(Vista.getService(0), sharedUrl!!)
|
// }
|
||||||
Logd(TAG, "info: $info")
|
// onDismissRequest()
|
||||||
val episode = episodeFromStreamInfo(info)
|
// }) {
|
||||||
Logd(TAG, "episode: $episode")
|
// Text("Confirm")
|
||||||
addToYoutubeSyndicate(episode, !audioOnly)
|
// }
|
||||||
}
|
// }
|
||||||
onDismissRequest()
|
// }
|
||||||
}) {
|
// }
|
||||||
Text("Confirm")
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNoPodcastFoundError() {
|
private fun showNoPodcastFoundError() {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
package ac.mdiq.podcini.ui.compose
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@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.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.status
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.status
|
||||||
import ac.mdiq.podcini.storage.database.Episodes
|
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.Episodes.setPlayState
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate
|
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
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
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.model.MediaType
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.EpisodeActionButton
|
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
|
||||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler.PutToQueueDialog
|
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler.PutToQueueDialog
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
||||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
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.text.format.Formatter
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.tween
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AddCircle
|
import androidx.compose.material.icons.filled.AddCircle
|
||||||
import androidx.compose.material.icons.filled.Edit
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import io.realm.kotlin.notifications.SingleQueryChange
|
import io.realm.kotlin.notifications.SingleQueryChange
|
||||||
import io.realm.kotlin.notifications.UpdatedObject
|
import io.realm.kotlin.notifications.UpdatedObject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import java.net.URL
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -86,10 +93,37 @@ fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction?>,
|
||||||
|
|
||||||
var queueChanged by mutableIntStateOf(0)
|
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)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, refreshCB: (()->Unit)? = null,
|
fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>, refreshCB: (()->Unit)? = null,
|
||||||
leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null, actionButton_: ((Episode)->EpisodeActionButton)? = null) {
|
leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null, actionButton_: ((Episode)-> EpisodeActionButton)? = null) {
|
||||||
val TAG = "EpisodeLazyColumn"
|
val TAG = "EpisodeLazyColumn"
|
||||||
var selectMode by remember { mutableStateOf(false) }
|
var selectMode by remember { mutableStateOf(false) }
|
||||||
var selectedSize by remember { mutableStateOf(0) }
|
var selectedSize by remember { mutableStateOf(0) }
|
||||||
|
@ -97,10 +131,16 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
var longPressIndex by remember { mutableIntStateOf(-1) }
|
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
|
@Composable
|
||||||
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
||||||
val TAG = "EpisodeSpeedDial ${selected.size}"
|
|
||||||
var isExpanded by remember { mutableStateOf(false) }
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
val options = mutableListOf<@Composable () -> Unit>(
|
val options = mutableListOf<@Composable () -> Unit>(
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
@ -193,7 +233,18 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "reserve: ${selected.size}")
|
Logd(TAG, "reserve: ${selected.size}")
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
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
|
}, verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
@ -216,66 +267,53 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
var refreshing by remember { mutableStateOf(false)}
|
var refreshing by remember { mutableStateOf(false)}
|
||||||
|
|
||||||
PullToRefreshBox(modifier = Modifier.fillMaxWidth(), isRefreshing = refreshing, indicator = {}, onRefresh = {
|
PullToRefreshBox(modifier = Modifier.fillMaxWidth(), isRefreshing = refreshing, indicator = {}, onRefresh = {
|
||||||
// coroutineScope.launch {
|
|
||||||
refreshing = true
|
refreshing = true
|
||||||
refreshCB?.invoke()
|
refreshCB?.invoke()
|
||||||
// if (swipeRefresh) FeedUpdateManager.runOnceOrAsk(activity)
|
|
||||||
refreshing = false
|
refreshing = false
|
||||||
// }
|
|
||||||
}) {
|
}) {
|
||||||
LazyColumn(state = lazyListState,
|
LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
itemsIndexed(episodes, key = {index, episode -> episode.id}) { index, episode ->
|
itemsIndexed(vms, key = {index, vm -> vm.episode.id}) { index, vm ->
|
||||||
var positionState by remember { mutableStateOf(episode.media?.position?:0) }
|
if (vm.episodeMonitor == null) {
|
||||||
var playedState by remember { mutableStateOf(episode.isPlayed()) }
|
vm.episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||||
var farvoriteState by remember { mutableStateOf(episode.isFavorite) }
|
val item_ = realm.query(Episode::class).query("id == ${vm.episode.id}").first()
|
||||||
var inProgressState by remember { mutableStateOf(episode.isInProgress) }
|
Logd(TAG, "start monitoring episode: $index ${vm.episode.title}")
|
||||||
|
|
||||||
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}")
|
|
||||||
val episodeFlow = item_.asFlow()
|
val episodeFlow = item_.asFlow()
|
||||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||||
when (changes) {
|
when (changes) {
|
||||||
is UpdatedObject -> {
|
is UpdatedObject -> {
|
||||||
Logd(TAG, "episodeMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
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 < vms.size && vms[index].episode.id == changes.obj.id) {
|
||||||
if (index < episodes.size && episodes[index].id == changes.obj.id) {
|
withContext(Dispatchers.Main) {
|
||||||
playedState = changes.obj.isPlayed()
|
vms[index].playedState = changes.obj.isPlayed()
|
||||||
farvoriteState = changes.obj.isFavorite
|
vms[index].farvoriteState = changes.obj.isFavorite
|
||||||
episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
vms[index].episode = 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)
|
|
||||||
}
|
}
|
||||||
|
Logd(TAG, "episodeMonitor $index ${vms[index].playedState} ${vm.playedState} ")
|
||||||
|
} else Logd(TAG, "episodeMonitor index out bound: $index ${vms.size}")
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mediaMonitor == null) {
|
if (vm.mediaMonitor == null) {
|
||||||
mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
|
vm.mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||||
val item_ = realm.query(Episode::class).query("id == ${episode.id}").first()
|
val item_ = realm.query(Episode::class).query("id == ${vm.episode.id}").first()
|
||||||
Logd(TAG, "start monitoring media: $index ${episode.title}")
|
Logd(TAG, "start monitoring media: $index ${vm.episode.title}")
|
||||||
val episodeFlow = item_.asFlow(listOf("media.*"))
|
val episodeFlow = item_.asFlow(listOf("media.*"))
|
||||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||||
when (changes) {
|
when (changes) {
|
||||||
is UpdatedObject -> {
|
is UpdatedObject -> {
|
||||||
Logd(TAG, "mediaMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
Logd(TAG, "mediaMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||||
if (index < episodes.size && episodes[index].id == changes.obj.id) {
|
if (index < vms.size && vms[index].episode.id == changes.obj.id) {
|
||||||
positionState = changes.obj.media?.position ?: 0
|
withContext(Dispatchers.Main) {
|
||||||
inProgressState = changes.obj.isInProgress
|
vms[index].positionState = changes.obj.media?.position ?: 0
|
||||||
// episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
vms[index].inProgressState = changes.obj.isInProgress
|
||||||
changes.obj.copyStates(episodes[index])
|
Logd(TAG, "mediaMonitor $index ${vm.positionState} ${vm.inProgressState} ${vm.episode.title}")
|
||||||
episodes.removeAt(index)
|
vms[index].episode = changes.obj
|
||||||
episodes.add(index, changes.obj)
|
|
||||||
}
|
}
|
||||||
|
} else Logd(TAG, "mediaMonitor index out bound: $index ${vms.size}")
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
@ -285,14 +323,16 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
Logd(TAG, "cancelling monitoring $index")
|
Logd(TAG, "cancelling monitoring $index")
|
||||||
episodeMonitor?.cancel()
|
vm.episodeMonitor?.cancel()
|
||||||
mediaMonitor?.cancel()
|
vm.mediaMonitor?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (episodes[index].stopMonitoring.value) {
|
LaunchedEffect(vm.actionButton) {
|
||||||
Logd(TAG, "cancel monitoring: ${episodes[index].title}")
|
Logd(TAG, "LaunchedEffect init actionButton")
|
||||||
episodeMonitor?.cancel()
|
if (vm.actionButton == null) {
|
||||||
mediaMonitor?.cancel()
|
vm.actionButton = if (actionButton_ != null) actionButton_(vm.episode) else EpisodeActionButton.forItem(vm.episode)
|
||||||
|
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val velocityTracker = remember { VelocityTracker() }
|
val velocityTracker = remember { VelocityTracker() }
|
||||||
val offsetX = remember { Animatable(0f) }
|
val offsetX = remember { Animatable(0f) }
|
||||||
|
@ -308,8 +348,11 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val velocity = velocityTracker.calculateVelocity().x
|
val velocity = velocityTracker.calculateVelocity().x
|
||||||
if (velocity > 1000f || velocity < -1000f) {
|
if (velocity > 1000f || velocity < -1000f) {
|
||||||
if (velocity > 0) rightSwipeCB?.invoke(episodes[index])
|
Logd(TAG, "velocity: $velocity")
|
||||||
else leftSwipeCB?.invoke(episodes[index])
|
// 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(
|
offsetX.animateTo(
|
||||||
targetValue = 0f, // Back to the initial position
|
targetValue = 0f, // Back to the initial position
|
||||||
|
@ -320,18 +363,17 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
)
|
)
|
||||||
}.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
}.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
||||||
) {
|
) {
|
||||||
var isSelected by remember { mutableStateOf(false) }
|
|
||||||
LaunchedEffect(key1 = selectMode, key2 = selectedSize) {
|
LaunchedEffect(key1 = selectMode, key2 = selectedSize) {
|
||||||
isSelected = selectMode && episode in selected
|
vm.isSelected = selectMode && vm.episode in selected
|
||||||
// Logd(TAG, "LaunchedEffect $index $isSelected ${selected.size}")
|
// Logd(TAG, "LaunchedEffect $index $isSelected ${selected.size}")
|
||||||
}
|
}
|
||||||
fun toggleSelected() {
|
fun toggleSelected() {
|
||||||
isSelected = !isSelected
|
vm.isSelected = !vm.isSelected
|
||||||
if (isSelected) selected.add(episodes[index])
|
if (vm.isSelected) selected.add(vms[index].episode)
|
||||||
else selected.remove(episodes[index])
|
else selected.remove(vms[index].episode)
|
||||||
}
|
}
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
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) {
|
if (false) {
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
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)) {
|
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||||
val (imgvCover, checkMark) = createRefs()
|
val (imgvCover, checkMark) = createRefs()
|
||||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode)
|
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
|
||||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
||||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||||
modifier = Modifier.width(56.dp).height(56.dp)
|
modifier = Modifier.width(56.dp).height(56.dp)
|
||||||
|
@ -352,10 +394,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||||
}.clickable(onClick = {
|
}.clickable(onClick = {
|
||||||
Logd(TAG, "icon clicked!")
|
Logd(TAG, "icon clicked!")
|
||||||
if (selectMode) toggleSelected()
|
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
|
val alpha = if (vm.playedState) 1.0f else 0f
|
||||||
if (playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
if (vm.playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||||
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||||
bottom.linkTo(parent.bottom)
|
bottom.linkTo(parent.bottom)
|
||||||
end.linkTo(parent.end)
|
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)
|
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
|
||||||
.combinedClickable(onClick = {
|
.combinedClickable(onClick = {
|
||||||
Logd(TAG, "clicked: ${episode.title}")
|
Logd(TAG, "clicked: ${vm.episode.title}")
|
||||||
if (selectMode) toggleSelected()
|
if (selectMode) toggleSelected()
|
||||||
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(episode))
|
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
|
||||||
}, onLongClick = {
|
}, onLongClick = {
|
||||||
selectMode = !selectMode
|
selectMode = !selectMode
|
||||||
isSelected = selectMode
|
vm.isSelected = selectMode
|
||||||
if (selectMode) {
|
if (selectMode) {
|
||||||
selected.add(episodes[index])
|
selected.add(vms[index].episode)
|
||||||
longPressIndex = index
|
longPressIndex = index
|
||||||
} else {
|
} else {
|
||||||
selected.clear()
|
selected.clear()
|
||||||
selectedSize = 0
|
selectedSize = 0
|
||||||
longPressIndex = -1
|
longPressIndex = -1
|
||||||
}
|
}
|
||||||
Logd(TAG, "long clicked: ${episode.title}")
|
Logd(TAG, "long clicked: ${vm.episode.title}")
|
||||||
})) {
|
})) {
|
||||||
var inQueueState by remember { mutableStateOf(false) }
|
|
||||||
LaunchedEffect(key1 = queueChanged) {
|
LaunchedEffect(key1 = queueChanged) {
|
||||||
if (index>=episodes.size) return@LaunchedEffect
|
if (index>=vms.size) return@LaunchedEffect
|
||||||
inQueueState = curQueue.contains(episodes[index])
|
vms[index].inQueueState = curQueue.contains(vms[index].episode)
|
||||||
}
|
}
|
||||||
val dur = episode.media!!.getDuration()
|
val dur = vm.episode.media!!.getDuration()
|
||||||
val durText = DurationConverter.getDurationStringLong(dur)
|
val durText = DurationConverter.getDurationStringLong(dur)
|
||||||
Row {
|
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))
|
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))
|
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))
|
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 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(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
Text(vm.episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||||
if (InTheatre.isCurMedia(episode.media) || inProgressState) {
|
if (InTheatre.isCurMedia(vm.episode.media) || vm.inProgressState) {
|
||||||
val pos = positionState
|
val pos = vm.positionState
|
||||||
val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||||
|
Logd(TAG, "$index vm.prog: ${vm.prog}")
|
||||||
Row {
|
Row {
|
||||||
Text(DurationConverter.getDurationStringLong(pos), color = textColor, style = MaterialTheme.typography.bodySmall)
|
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||||
LinearProgressIndicator(progress = { prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
||||||
Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall)
|
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 {
|
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) {
|
if (actionButton_ == null) {
|
||||||
LaunchedEffect(episodes[index].downloadState.value) {
|
LaunchedEffect(vms[index].downloadState) {
|
||||||
if (index>=episodes.size) return@LaunchedEffect
|
if (index>=vms.size) return@LaunchedEffect
|
||||||
if (isDownloading()) dlPercent = dls?.getProgress(episodes[index].media!!.downloadUrl!!) ?: 0
|
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media!!.downloadUrl!!) ?: 0
|
||||||
// Logd(TAG, "downloadState: ${episodes[index].downloadState.value} ${episode.media?.downloaded} $dlPercent")
|
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
|
||||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
|
||||||
actionRes = actionButton.getDrawable()
|
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||||
}
|
}
|
||||||
LaunchedEffect(key1 = status) {
|
LaunchedEffect(key1 = status) {
|
||||||
if (index>=episodes.size) return@LaunchedEffect
|
if (index>=vms.size) return@LaunchedEffect
|
||||||
// Logd(TAG, "$index isPlayingState: ${episodes[index].isPlayingState.value} ${episodes[index].title}")
|
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}")
|
||||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
|
||||||
actionRes = actionButton.getDrawable()
|
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) {
|
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically).pointerInput(Unit) {
|
||||||
detectTapGestures(onLongPress = { showAltActionsDialog = true }, onTap = {
|
detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = {
|
||||||
actionButton.onClick(activity)
|
vm.actionButton?.onClick(activity)
|
||||||
})
|
})
|
||||||
}, contentAlignment = Alignment.Center) {
|
}, contentAlignment = Alignment.Center) {
|
||||||
// actionRes = actionButton.getDrawable()
|
// actionRes = actionButton.getDrawable()
|
||||||
Icon(painter = painterResource(actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
Icon(painter = painterResource(vm.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))
|
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 = {
|
.clickable(onClick = {
|
||||||
selected.clear()
|
selected.clear()
|
||||||
for (i in 0..longPressIndex) {
|
for (i in 0..longPressIndex) {
|
||||||
selected.add(episodes[i])
|
selected.add(vms[i].episode)
|
||||||
}
|
}
|
||||||
selectedSize = selected.size
|
selectedSize = selected.size
|
||||||
Logd(TAG, "selectedIds: ${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)
|
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 = {
|
.clickable(onClick = {
|
||||||
selected.clear()
|
selected.clear()
|
||||||
for (i in longPressIndex..<episodes.size) {
|
for (i in longPressIndex..<vms.size) {
|
||||||
selected.add(episodes[i])
|
selected.add(vms[i].episode)
|
||||||
}
|
}
|
||||||
selectedSize = selected.size
|
selectedSize = selected.size
|
||||||
Logd(TAG, "selectedIds: ${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) }
|
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)
|
Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
if (selectedSize != episodes.size) {
|
if (selectedSize != vms.size) {
|
||||||
selected.clear()
|
selected.clear()
|
||||||
selected.addAll(episodes)
|
for (vm in vms) {
|
||||||
// for (e in episodes) {
|
selected.add(vm.episode)
|
||||||
// selected.add(e)
|
}
|
||||||
// }
|
|
||||||
selectAllRes = R.drawable.ic_select_none
|
selectAllRes = R.drawable.ic_select_none
|
||||||
} else {
|
} else {
|
||||||
selected.clear()
|
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.FeedBuilder
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
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.ui.fragment.OnlineFeedFragment
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -35,16 +37,8 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
|
||||||
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||||
if (showDialog) {
|
if (showDialog) {
|
||||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||||
Card(
|
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||||
modifier = Modifier
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||||
.wrapContentSize(align = Alignment.Center)
|
|
||||||
.padding(16.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text("Subscribe: \"${feed.title}\" ?")
|
Text("Subscribe: \"${feed.title}\" ?")
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
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(
|
Column(Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (feed.feedUrl != null) {
|
if (feed.feedUrl != null) {
|
||||||
|
if (feed.feedId > 0) {
|
||||||
|
val fragment = FeedEpisodesFragment.newInstance(feed.feedId)
|
||||||
|
activity.loadChildFragment(fragment)
|
||||||
|
} else {
|
||||||
val fragment = OnlineFeedFragment.newInstance(feed.feedUrl)
|
val fragment = OnlineFeedFragment.newInstance(feed.feedUrl)
|
||||||
fragment.feedSource = feed.source
|
fragment.feedSource = feed.source
|
||||||
activity.loadChildFragment(fragment)
|
activity.loadChildFragment(fragment)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, onLongClick = { showSubscribeDialog.value = true })) {
|
}, onLongClick = { showSubscribeDialog.value = true })) {
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
|
Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
|
||||||
Row {
|
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)) {
|
Column(Modifier.padding(start = 10.dp)) {
|
||||||
var authorText by remember { mutableStateOf("") }
|
var authorText by remember { mutableStateOf("") }
|
||||||
authorText = when {
|
authorText = when {
|
||||||
|
|
|
@ -14,11 +14,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.*
|
import ac.mdiq.podcini.databinding.*
|
||||||
import ac.mdiq.podcini.ui.fragment.*
|
import ac.mdiq.podcini.ui.fragment.*
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.getPrefsWithDefaults
|
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.getPrefsWithDefaults
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.getSharedPrefs
|
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.getSharedPrefs
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.isSwipeActionEnabled
|
import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.isSwipeActionEnabled
|
||||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
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.MainActivity
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||||
|
@ -61,7 +61,6 @@ import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.R
|
||||||
import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding
|
import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding
|
||||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
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.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.*
|
||||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
|
||||||
import ac.mdiq.podcini.ui.compose.InforBar
|
|
||||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
@ -21,6 +18,7 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
|
@ -54,7 +52,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||||
lateinit var toolbar: MaterialToolbar
|
lateinit var toolbar: MaterialToolbar
|
||||||
lateinit var swipeActions: SwipeActions
|
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 {
|
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
@ -79,19 +78,24 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||||
|
|
||||||
swipeActions = SwipeActions(this, TAG)
|
swipeActions = SwipeActions(this, TAG)
|
||||||
lifecycle.addObserver(swipeActions)
|
lifecycle.addObserver(swipeActions)
|
||||||
binding.infobar.setContent {
|
|
||||||
CustomTheme(requireContext()) {
|
|
||||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.lazyColumn.setContent {
|
binding.lazyColumn.setContent {
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
|
Column {
|
||||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
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())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
swipeActions.setFilter(getFilter())
|
swipeActions.setFilter(getFilter())
|
||||||
refreshSwipeTelltale()
|
refreshSwipeTelltale()
|
||||||
|
@ -143,6 +147,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
_binding = null
|
||||||
episodes.clear()
|
episodes.clear()
|
||||||
|
vms.clear()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,8 +165,10 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||||
for (url in event.urls) {
|
for (url in event.urls) {
|
||||||
// if (!event.isCompleted(url)) continue
|
// if (!event.isCompleted(url)) continue
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, url)
|
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) {
|
val data = withContext(Dispatchers.IO) {
|
||||||
Pair(loadData().toMutableList(), loadTotalItemCount())
|
Pair(loadData().toMutableList(), loadTotalItemCount())
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
val restoreScrollPosition = episodes.isEmpty()
|
val restoreScrollPosition = episodes.isEmpty()
|
||||||
episodes.clear()
|
episodes.clear()
|
||||||
episodes.addAll(data.first)
|
episodes.addAll(data.first)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
vms.clear()
|
||||||
|
for (e in data.first) { vms.add(EpisodeVM(e)) }
|
||||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
|
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
|
||||||
updateToolbar()
|
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.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.DownloadResultComparator
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog
|
import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog
|
||||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
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.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
|
import ac.mdiq.podcini.ui.actions.DeleteActionButton
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.*
|
||||||
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.dialog.EpisodeFilterDialog
|
import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
|
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
|
||||||
|
@ -38,6 +35,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -61,7 +59,8 @@ import java.util.*
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var runningDownloads: Set<String> = HashSet()
|
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 infoBarText = mutableStateOf("")
|
||||||
private var leftActionState = mutableStateOf<SwipeAction?>(null)
|
private var leftActionState = mutableStateOf<SwipeAction?>(null)
|
||||||
|
@ -93,18 +92,21 @@ import java.util.*
|
||||||
|
|
||||||
swipeActions = SwipeActions(this, TAG)
|
swipeActions = SwipeActions(this, TAG)
|
||||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||||
binding.infobar.setContent {
|
|
||||||
CustomTheme(requireContext()) {
|
|
||||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.lazyColumn.setContent {
|
binding.lazyColumn.setContent {
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
|
Column {
|
||||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
EpisodeLazyColumn(activity as MainActivity, vms = vms,
|
||||||
actionButton_ = { DeleteActionButton(it) } )
|
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)
|
// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||||
|
@ -143,11 +145,10 @@ import java.util.*
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
_binding = null
|
||||||
// adapter.endSelectMode()
|
|
||||||
// adapter.clearData()
|
|
||||||
toolbar.setOnMenuItemClickListener(null)
|
toolbar.setOnMenuItemClickListener(null)
|
||||||
toolbar.setOnLongClickListener(null)
|
toolbar.setOnLongClickListener(null)
|
||||||
episodes.clear()
|
episodes.clear()
|
||||||
|
vms.clear()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,8 +296,12 @@ import java.util.*
|
||||||
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||||
if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
episodes.removeAt(pos)
|
episodes.removeAt(pos)
|
||||||
|
vms.removeAt(pos)
|
||||||
val media = item.media
|
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
|
// 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)
|
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||||
if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
episodes.removeAt(pos)
|
episodes.removeAt(pos)
|
||||||
|
vms.removeAt(pos)
|
||||||
val media = item.media
|
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
|
// 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 var loadItemsRunning = false
|
||||||
private fun loadItems() {
|
private fun loadItems() {
|
||||||
emptyView.hide()
|
emptyView.hide()
|
||||||
|
Logd(TAG, "loadItems() called")
|
||||||
if (!loadItemsRunning) {
|
if (!loadItemsRunning) {
|
||||||
loadItemsRunning = true
|
loadItemsRunning = true
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -353,6 +363,10 @@ import java.util.*
|
||||||
episodes.addAll(currentDownloads)
|
episodes.addAll(currentDownloads)
|
||||||
}
|
}
|
||||||
episodes.retainAll { filter.matchesForQueues(it) }
|
episodes.retainAll { filter.matchesForQueues(it) }
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
vms.clear()
|
||||||
|
for (e in episodes) vms.add(EpisodeVM(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) { refreshInfoBar() }
|
withContext(Dispatchers.Main) { refreshInfoBar() }
|
||||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
@ -364,13 +378,13 @@ import java.util.*
|
||||||
private fun getEpisdesWithUrl(urls: List<String>): List<Episode> {
|
private fun getEpisdesWithUrl(urls: List<String>): List<Episode> {
|
||||||
Logd(TAG, "getEpisdesWithUrl() called ")
|
Logd(TAG, "getEpisdesWithUrl() called ")
|
||||||
if (urls.isEmpty()) return listOf()
|
if (urls.isEmpty()) return listOf()
|
||||||
val episodes: MutableList<Episode> = mutableListOf()
|
val episodes_: MutableList<Episode> = mutableListOf()
|
||||||
for (url in urls) {
|
for (url in urls) {
|
||||||
val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue
|
val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue
|
||||||
val item_ = media.episodeOrFetch()
|
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() {
|
private fun refreshInfoBar() {
|
||||||
|
@ -380,7 +394,7 @@ import java.util.*
|
||||||
for (item in episodes) sizeMB += item.media?.size ?: 0
|
for (item in episodes) sizeMB += item.media?.size ?: 0
|
||||||
info += " • " + (sizeMB / 1000000) + " MB"
|
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)}"
|
if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}"
|
||||||
infoBarText.value = info
|
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.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.*
|
import ac.mdiq.podcini.ui.actions.*
|
||||||
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.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
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.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
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.EpisodeSortOrder.Companion.fromCode
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.*
|
import ac.mdiq.podcini.ui.compose.*
|
||||||
import ac.mdiq.podcini.ui.dialog.*
|
import ac.mdiq.podcini.ui.dialog.*
|
||||||
|
@ -79,7 +79,10 @@ import java.util.concurrent.Semaphore
|
||||||
private var headerCreated = false
|
private var headerCreated = false
|
||||||
private var feedID: Long = 0
|
private var feedID: Long = 0
|
||||||
private var feed by mutableStateOf<Feed?>(null)
|
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 ieMap: Map<Long, Int> = mapOf()
|
||||||
private var ueMap: Map<String, Int> = mapOf()
|
private var ueMap: Map<String, Int> = mapOf()
|
||||||
|
|
||||||
|
@ -143,6 +146,8 @@ import java.util.concurrent.Semaphore
|
||||||
episodes.addAll(etmp)
|
episodes.addAll(etmp)
|
||||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
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
|
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()})
|
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 {
|
binding.lazyColumn.setContent {
|
||||||
|
Column {
|
||||||
|
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes, refreshCB = {FeedUpdateManager.runOnceOrAsk(requireContext(), feed)},
|
EpisodeLazyColumn(activity as MainActivity, vms = vms,
|
||||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
|
||||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}, )
|
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()
|
ieMap = mapOf()
|
||||||
ueMap = mapOf()
|
ueMap = mapOf()
|
||||||
episodes.clear()
|
episodes.clear()
|
||||||
|
vms.clear()
|
||||||
tts?.stop()
|
tts?.stop()
|
||||||
tts?.shutdown()
|
tts?.shutdown()
|
||||||
ttsWorking = false
|
ttsWorking = false
|
||||||
|
@ -384,14 +396,12 @@ import java.util.concurrent.Semaphore
|
||||||
|
|
||||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||||
// Logd(TAG, "onPlayEvent ${event.episode.title}")
|
// Logd(TAG, "onPlayEvent ${event.episode.title}")
|
||||||
// if (feed != null) {
|
if (feed != null) {
|
||||||
// val pos: Int = ieMap[event.episode.id] ?: -1
|
val pos: Int = ieMap[event.episode.id] ?: -1
|
||||||
// if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
//// if (!filterOutEpisode(event.episode)) episodes[pos].isPlayingState.value = event.isPlaying()
|
if (!filterOutEpisode(event.episode)) vms[pos].isPlayingState = event.isPlaying()
|
||||||
//// if (filterOutEpisode(event.episode)) adapter.updateItems(episodes)
|
}
|
||||||
//// else adapter.notifyItemChangedCompat(pos)
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||||
|
@ -403,8 +413,8 @@ import java.util.concurrent.Semaphore
|
||||||
val pos: Int = ueMap[url] ?: -1
|
val pos: Int = ueMap[url] ?: -1
|
||||||
if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
Logd(TAG, "onEpisodeDownloadEvent $pos ${event.map[url]?.state} ${episodes[pos].media?.downloaded} ${episodes[pos].title}")
|
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
|
// episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||||
// adapter.notifyItemChangedCompat(pos)
|
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)
|
while (loadItemsRunning) Thread.sleep(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun filterOutEpisode(episode: Episode): Boolean {
|
private fun filterOutEpisode(episode: Episode): Boolean {
|
||||||
// if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty() && !feed!!.episodeFilter.matches(episode)) {
|
if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty() && !feed!!.episodeFilter.matches(episode)) {
|
||||||
// episodes.remove(episode)
|
episodes.remove(episode)
|
||||||
// ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||||
// ueMap = episodes.mapIndexedNotNull { index, episode_ -> episode_.media?.downloadUrl?.let { it to index } }.toMap()
|
ueMap = episodes.mapIndexedNotNull { index, episode_ -> episode_.media?.downloadUrl?.let { it to index } }.toMap()
|
||||||
// return true
|
return true
|
||||||
// }
|
}
|
||||||
// return false
|
return false
|
||||||
// }
|
}
|
||||||
|
|
||||||
// private fun redoFilter(list: List<Episode>? = null) {
|
// private fun redoFilter(list: List<Episode>? = null) {
|
||||||
// if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty()) {
|
// if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty()) {
|
||||||
|
@ -574,6 +584,10 @@ import java.util.concurrent.Semaphore
|
||||||
episodes.addAll(etmp)
|
episodes.addAll(etmp)
|
||||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
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) {
|
if (onInit) {
|
||||||
var hasNonMediaItems = false
|
var hasNonMediaItems = false
|
||||||
for (item in episodes) {
|
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.getFeed
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
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.storage.model.*
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
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.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
@ -23,7 +22,6 @@ import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.LightingColorFilter
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
@ -34,15 +32,24 @@ import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.collection.ArrayMap
|
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.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -72,6 +79,13 @@ class OnlineFeedFragment : Fragment() {
|
||||||
private var feedUrl: String = ""
|
private var feedUrl: String = ""
|
||||||
private lateinit var feedBuilder: FeedBuilder
|
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
|
private val feedId: Long
|
||||||
get() {
|
get() {
|
||||||
if (feeds == null) return 0
|
if (feeds == null) return 0
|
||||||
|
@ -83,6 +97,7 @@ class OnlineFeedFragment : Fragment() {
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var feeds: List<Feed>? = null
|
private var feeds: List<Feed>? = null
|
||||||
|
private var feed by mutableStateOf<Feed?>(null)
|
||||||
private var selectedDownloadUrl: String? = null
|
private var selectedDownloadUrl: String? = null
|
||||||
// private var downloader: Downloader? = null
|
// private var downloader: Downloader? = null
|
||||||
private var username: String? = 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 {
|
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
Logd(TAG, "fragment onCreateView")
|
Logd(TAG, "fragment onCreateView")
|
||||||
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
_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
|
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||||
|
|
||||||
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
||||||
Logd(TAG, "feedUrl: $feedUrl")
|
Logd(TAG, "feedUrl: $feedUrl")
|
||||||
|
|
||||||
feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) }
|
feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) }
|
||||||
|
|
||||||
|
binding.mainView.setContent {
|
||||||
|
CustomTheme(requireContext()) {
|
||||||
|
MainView()
|
||||||
|
}
|
||||||
|
}
|
||||||
if (feedUrl.isEmpty()) {
|
if (feedUrl.isEmpty()) {
|
||||||
Log.e(TAG, "feedUrl is null.")
|
Log.e(TAG, "feedUrl is null.")
|
||||||
showNoPodcastFoundError()
|
showNoPodcastFoundError()
|
||||||
} else {
|
} else {
|
||||||
Logd(TAG, "Activity was started with url $feedUrl")
|
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
|
// 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 (feedUrl.contains("subscribeonandroid.com")) feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "")
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
@ -127,11 +142,6 @@ class OnlineFeedFragment : Fragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoadingLayout() {
|
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
|
||||||
binding.feedDisplayContainer.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
isPaused = false
|
isPaused = false
|
||||||
|
@ -164,9 +174,10 @@ class OnlineFeedFragment : Fragment() {
|
||||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||||
try {
|
try {
|
||||||
feeds = getFeedList()
|
feeds = getFeedList()
|
||||||
feedBuilder.startFeedBuilding(urlString, username, password) { feed, map ->
|
feedBuilder.startFeedBuilding(urlString, username, password) { feed_, map ->
|
||||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||||
showFeedInformation(feed, map)
|
feed = feed_
|
||||||
|
showFeedInformation(feed_, map)
|
||||||
}
|
}
|
||||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -196,9 +207,10 @@ class OnlineFeedFragment : Fragment() {
|
||||||
Logd(TAG, "Successfully retrieve feed url")
|
Logd(TAG, "Successfully retrieve feed url")
|
||||||
isFeedFoundBySearch = true
|
isFeedFoundBySearch = true
|
||||||
feeds = getFeedList()
|
feeds = getFeedList()
|
||||||
feedBuilder.startFeedBuilding(url, username, password) { feed, map ->
|
feedBuilder.startFeedBuilding(url, username, password) { feed_, map ->
|
||||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||||
showFeedInformation(feed, map)
|
feed = feed_
|
||||||
|
showFeedInformation(feed_, map)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showNoPodcastFoundError()
|
showNoPodcastFoundError()
|
||||||
|
@ -268,77 +280,140 @@ class OnlineFeedFragment : Fragment() {
|
||||||
* This method is executed on the GUI thread.
|
* This method is executed on the GUI thread.
|
||||||
*/
|
*/
|
||||||
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
||||||
binding.progressBar.visibility = View.GONE
|
showProgress = false
|
||||||
binding.feedDisplayContainer.visibility = View.VISIBLE
|
// binding.feedDisplayContainer.visibility = View.VISIBLE
|
||||||
|
showFeedDisplay = true
|
||||||
if (isFeedFoundBySearch) {
|
if (isFeedFoundBySearch) {
|
||||||
val resId = R.string.no_feed_url_podcast_found_by_search
|
val resId = R.string.no_feed_url_podcast_found_by_search
|
||||||
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
binding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
|
||||||
binding.episodeLabel.setOnClickListener { showEpisodes(feed.episodes)}
|
// if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
||||||
if (!feed.imageUrl.isNullOrBlank()) {
|
// else {
|
||||||
binding.coverImage.load(feed.imageUrl) {
|
// binding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||||
placeholder(R.color.light_gray)
|
// val alternateUrlsList: MutableList<String> = ArrayList()
|
||||||
error(R.mipmap.ic_launcher)
|
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
}
|
}
|
||||||
binding.titleLabel.text = feed.title
|
Row(Modifier.constrainAs(buttons) {
|
||||||
binding.authorLabel.text = feed.author
|
start.linkTo(coverImage.end)
|
||||||
binding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
|
top.linkTo(taColumn.bottom)
|
||||||
binding.txtvTechInfo.text = "${feed.episodes.size} episodes\n" +
|
end.linkTo(parent.end)
|
||||||
"${feed.mostRecentItem?.title ?: ""}\n\n" +
|
}) {
|
||||||
"${feed.language} ${feed.type ?: ""} ${feed.lastUpdate ?: ""}\n" +
|
Spacer(modifier = Modifier.weight(0.2f))
|
||||||
"${feed.link}\n" +
|
if (enableSubscribe) Button(onClick = {
|
||||||
"${feed.downloadUrl}"
|
if (feedInFeedlist()) (activity as MainActivity).loadFeedFragmentById(feedId, null)
|
||||||
binding.subscribeButton.setOnClickListener {
|
|
||||||
if (feedInFeedlist()) openFeed()
|
|
||||||
else {
|
else {
|
||||||
lifecycleScope.launch {
|
enableSubscribe = false
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
enableEpisodes = false
|
||||||
withContext(Dispatchers.IO) { feedBuilder.subscribe(feed) }
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
feedBuilder.subscribe(feed!!)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.progressBar.visibility = View.GONE
|
enableSubscribe = true
|
||||||
didPressSubscribe = true
|
didPressSubscribe = true
|
||||||
handleUpdatedFeedStatus()
|
handleUpdatedFeedStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(subButTextRes))
|
||||||
}
|
}
|
||||||
if (feedSource != "VistaGuide" && isEnableAutodownload)
|
Spacer(modifier = Modifier.weight(0.1f))
|
||||||
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) {
|
||||||
|
Text(stringResource(R.string.episodes_label))
|
||||||
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(),
|
Spacer(modifier = Modifier.weight(0.2f))
|
||||||
R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
}
|
||||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
if (false) Icon(painter = painterResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier
|
||||||
// reusing the old view causes a visual bug on Android <= 10
|
.constrainAs(closeButton) {
|
||||||
return super.getDropDownView(position, null, parent)
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
val scrollState = rememberScrollState()
|
||||||
binding.alternateUrlsSpinner.adapter = adapter
|
var numEpisodes by remember { mutableIntStateOf(feed?.episodes?.size?:0) }
|
||||||
binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
LaunchedEffect(Unit) {
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
while (true) {
|
||||||
selectedDownloadUrl = alternateUrlsList[position]
|
delay(1000)
|
||||||
}
|
numEpisodes = feed?.episodes?.size?:0
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleUpdatedFeedStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun openFeed() {
|
|
||||||
(activity as MainActivity).loadFeedFragmentById(feedId, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun showEpisodes(episodes: MutableList<Episode>) {
|
@UnstableApi private fun showEpisodes(episodes: MutableList<Episode>) {
|
||||||
|
@ -364,12 +439,12 @@ class OnlineFeedFragment : Fragment() {
|
||||||
// binding.subscribeButton.isEnabled = false
|
// binding.subscribeButton.isEnabled = false
|
||||||
// }
|
// }
|
||||||
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
|
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
|
||||||
binding.subscribeButton.isEnabled = false
|
enableSubscribe = false
|
||||||
binding.subscribeButton.setText(R.string.subscribing_label)
|
subButTextRes = R.string.subscribing_label
|
||||||
}
|
}
|
||||||
feedInFeedlist() -> {
|
feedInFeedlist() -> {
|
||||||
binding.subscribeButton.isEnabled = true
|
enableSubscribe = true
|
||||||
binding.subscribeButton.setText(R.string.open)
|
subButTextRes = R.string.open
|
||||||
if (didPressSubscribe) {
|
if (didPressSubscribe) {
|
||||||
didPressSubscribe = false
|
didPressSubscribe = false
|
||||||
val feed1 = getFeed(feedId, true)?: return
|
val feed1 = getFeed(feedId, true)?: return
|
||||||
|
@ -379,7 +454,7 @@ class OnlineFeedFragment : Fragment() {
|
||||||
feed1.preferences!!.prefStreamOverDownload = true
|
feed1.preferences!!.prefStreamOverDownload = true
|
||||||
feed1.preferences!!.autoDownload = false
|
feed1.preferences!!.autoDownload = false
|
||||||
} else if (isEnableAutodownload) {
|
} else if (isEnableAutodownload) {
|
||||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
val autoDownload = autoDownloadChecked
|
||||||
feed1.preferences!!.autoDownload = autoDownload
|
feed1.preferences!!.autoDownload = autoDownload
|
||||||
val editor = prefs!!.edit()
|
val editor = prefs!!.edit()
|
||||||
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
||||||
|
@ -390,13 +465,12 @@ class OnlineFeedFragment : Fragment() {
|
||||||
feed1.preferences!!.password = password
|
feed1.preferences!!.password = password
|
||||||
}
|
}
|
||||||
persistFeedPreferences(feed1)
|
persistFeedPreferences(feed1)
|
||||||
openFeed()
|
// openFeed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
binding.subscribeButton.isEnabled = true
|
enableSubscribe = true
|
||||||
binding.subscribeButton.setText(R.string.subscribe_label)
|
subButTextRes = R.string.subscribing_label
|
||||||
if (feedSource != "VistaGuide" && isEnableAutodownload) binding.autoDownloadCheckBox.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,10 @@ import ac.mdiq.podcini.storage.model.PlayQueue
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.*
|
||||||
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.dialog.ConfirmationDialog
|
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
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.android.material.snackbar.Snackbar
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
@ -113,7 +109,8 @@ import kotlin.math.max
|
||||||
private lateinit var queues: List<PlayQueue>
|
private lateinit var queues: List<PlayQueue>
|
||||||
|
|
||||||
private var displayUpArrow = false
|
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)
|
private var showBin by mutableStateOf(false)
|
||||||
|
|
||||||
|
@ -183,7 +180,7 @@ import kotlin.math.max
|
||||||
|
|
||||||
swipeActions = SwipeActions(this, TAG)
|
swipeActions = SwipeActions(this, TAG)
|
||||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
||||||
swipeActionsBin = SwipeActions(this, TAG)
|
swipeActionsBin = SwipeActions(this, TAG+".Bin")
|
||||||
swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
||||||
|
|
||||||
binding.lazyColumn.setContent {
|
binding.lazyColumn.setContent {
|
||||||
|
@ -199,7 +196,7 @@ import kotlin.math.max
|
||||||
if (rightActionStateBin.value == null) swipeActionsBin.showDialog()
|
if (rightActionStateBin.value == null) swipeActionsBin.showDialog()
|
||||||
else rightActionStateBin.value?.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
|
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 {
|
} else {
|
||||||
Column {
|
Column {
|
||||||
|
@ -212,7 +209,7 @@ import kotlin.math.max
|
||||||
if (rightActionState.value == null) swipeActions.showDialog()
|
if (rightActionState.value == null) swipeActions.showDialog()
|
||||||
else rightActionState.value?.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
|
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
|
if (showBin) return
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
FlowEvent.QueueEvent.Action.ADDED -> {
|
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 -> {
|
FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> {
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
queueItems.addAll(event.episodes)
|
queueItems.addAll(event.episodes)
|
||||||
|
vms.clear()
|
||||||
|
for (e in event.episodes) vms.add(EpisodeVM(e))
|
||||||
}
|
}
|
||||||
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
|
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
|
||||||
if (event.episodes.isNotEmpty()) {
|
if (event.episodes.isNotEmpty()) {
|
||||||
|
@ -329,8 +331,10 @@ import kotlin.math.max
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
|
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
|
||||||
if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
|
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
|
||||||
queueItems[pos].stopMonitoring.value = true
|
// queueItems[pos].stopMonitoring.value = true
|
||||||
queueItems.removeAt(pos)
|
queueItems.removeAt(pos)
|
||||||
|
vms[pos].stopMonitoring()
|
||||||
|
vms.removeAt(pos)
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
|
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
|
||||||
continue
|
continue
|
||||||
|
@ -344,6 +348,7 @@ import kotlin.math.max
|
||||||
}
|
}
|
||||||
FlowEvent.QueueEvent.Action.CLEARED -> {
|
FlowEvent.QueueEvent.Action.CLEARED -> {
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
|
vms.clear()
|
||||||
}
|
}
|
||||||
FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return
|
FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return
|
||||||
}
|
}
|
||||||
|
@ -357,7 +362,7 @@ import kotlin.math.max
|
||||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
|
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
|
||||||
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
|
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) {
|
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||||
|
@ -366,7 +371,10 @@ import kotlin.math.max
|
||||||
for (url in event.urls) {
|
for (url in event.urls) {
|
||||||
// if (!event.isCompleted(url)) continue
|
// if (!event.isCompleted(url)) continue
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url)
|
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")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
_binding = null
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
|
vms.clear()
|
||||||
toolbar.setOnMenuItemClickListener(null)
|
toolbar.setOnMenuItemClickListener(null)
|
||||||
toolbar.setOnLongClickListener(null)
|
toolbar.setOnLongClickListener(null)
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -662,12 +671,14 @@ import kotlin.math.max
|
||||||
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
||||||
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
|
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
|
vms.clear()
|
||||||
if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
|
if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
|
||||||
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
|
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
|
||||||
else {
|
else {
|
||||||
curQueue.episodes.clear()
|
curQueue.episodes.clear()
|
||||||
queueItems.addAll(curQueue.episodes)
|
queueItems.addAll(curQueue.episodes)
|
||||||
}
|
}
|
||||||
|
for (e in queueItems) vms.add(EpisodeVM(e))
|
||||||
Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}")
|
Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}")
|
||||||
|
|
||||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
// 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.Episode
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
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.CustomFeedNameDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
||||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||||
import ac.mdiq.podcini.ui.view.SquareImageView
|
import ac.mdiq.podcini.ui.view.SquareImageView
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -68,7 +69,8 @@ class SearchFragment : Fragment() {
|
||||||
private lateinit var chip: Chip
|
private lateinit var chip: Chip
|
||||||
private lateinit var automaticSearchDebouncer: Handler
|
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 lastQueryChange: Long = 0
|
||||||
private var isOtherViewInFoucus = false
|
private var isOtherViewInFoucus = false
|
||||||
|
@ -86,7 +88,7 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
binding.lazyColumn.setContent {
|
binding.lazyColumn.setContent {
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
EpisodeLazyColumn(activity as MainActivity, episodes = results)
|
EpisodeLazyColumn(activity as MainActivity, vms = vms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +139,7 @@ class SearchFragment : Fragment() {
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
_binding = null
|
||||||
results.clear()
|
results.clear()
|
||||||
|
vms.clear()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +232,10 @@ class SearchFragment : Fragment() {
|
||||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||||
for (url in event.urls) {
|
for (url in event.urls) {
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, url)
|
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()
|
val first_ = results_.first!!.toMutableList()
|
||||||
results.clear()
|
results.clear()
|
||||||
results.addAll(first_)
|
results.addAll(first_)
|
||||||
|
vms.clear()
|
||||||
|
for (e in first_) { vms.add(EpisodeVM(e)) }
|
||||||
}
|
}
|
||||||
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
||||||
if (results_.second != null) adapterFeeds.updateData(results_.second!!)
|
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.PodcastSearchResult
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
|
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -20,8 +20,10 @@ import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.*
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
@ -31,15 +33,12 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -168,8 +167,18 @@ class SearchResultsFragment : Fragment() {
|
||||||
private fun search(query: String) {
|
private fun search(query: String) {
|
||||||
showOnlyProgressBar()
|
showOnlyProgressBar()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
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 {
|
try {
|
||||||
val result = searchProvider?.search(query) ?: listOf()
|
val result = searchProvider?.search(query) ?: listOf()
|
||||||
|
for (r in result) {
|
||||||
|
r.feedId = feedId(r)
|
||||||
|
}
|
||||||
searchResults.clear()
|
searchResults.clear()
|
||||||
searchResults.addAll(result)
|
searchResults.addAll(result)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
|
@ -847,9 +847,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
Column(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
Column(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||||
.combinedClickable(onClick = {
|
.combinedClickable(onClick = {
|
||||||
Logd(TAG, "clicked: ${feed.title}")
|
Logd(TAG, "clicked: ${feed.title}")
|
||||||
|
if (!feed.isBuilding) {
|
||||||
if (selectMode) toggleSelected()
|
if (selectMode) toggleSelected()
|
||||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||||
|
}
|
||||||
}, onLongClick = {
|
}, onLongClick = {
|
||||||
|
if (!feed.isBuilding) {
|
||||||
selectMode = !selectMode
|
selectMode = !selectMode
|
||||||
isSelected = selectMode
|
isSelected = selectMode
|
||||||
if (selectMode) {
|
if (selectMode) {
|
||||||
|
@ -859,6 +862,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
selectedSize = 0
|
selectedSize = 0
|
||||||
longPressIndex = -1
|
longPressIndex = -1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Logd(TAG, "long clicked: ${feed.title}")
|
Logd(TAG, "long clicked: ${feed.title}")
|
||||||
})) {
|
})) {
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
@ -914,16 +918,21 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
modifier = Modifier.width(80.dp).height(80.dp)
|
modifier = Modifier.width(80.dp).height(80.dp)
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
Logd(TAG, "icon clicked!")
|
Logd(TAG, "icon clicked!")
|
||||||
|
if (!feed.isBuilding) {
|
||||||
if (selectMode) toggleSelected()
|
if (selectMode) toggleSelected()
|
||||||
else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = {
|
Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = {
|
||||||
Logd(TAG, "clicked: ${feed.title}")
|
Logd(TAG, "clicked: ${feed.title}")
|
||||||
|
if (!feed.isBuilding) {
|
||||||
if (selectMode) toggleSelected()
|
if (selectMode) toggleSelected()
|
||||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||||
|
}
|
||||||
}, onLongClick = {
|
}, onLongClick = {
|
||||||
|
if (!feed.isBuilding) {
|
||||||
selectMode = !selectMode
|
selectMode = !selectMode
|
||||||
isSelected = selectMode
|
isSelected = selectMode
|
||||||
if (selectMode) {
|
if (selectMode) {
|
||||||
|
@ -934,6 +943,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
selectedSize = 0
|
selectedSize = 0
|
||||||
longPressIndex = -1
|
longPressIndex = -1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Logd(TAG, "long clicked: ${feed.title}")
|
Logd(TAG, "long clicked: ${feed.title}")
|
||||||
})) {
|
})) {
|
||||||
Text(feed.title ?: "No title", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
Text(feed.title ?: "No title", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -944,9 +954,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
color = textColor, style = MaterialTheme.typography.bodyMedium)
|
color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
var feedSortInfo by remember { mutableStateOf(feed.sortInfo) }
|
var feedSortInfo by remember { mutableStateOf(feed.sortInfo) }
|
||||||
LaunchedEffect(feedSorted) {
|
LaunchedEffect(feedSorted) { feedSortInfo = feed.sortInfo }
|
||||||
feedSortInfo = feed.sortInfo
|
|
||||||
}
|
|
||||||
Text(feedSortInfo, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
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.R
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils
|
import ac.mdiq.podcini.net.utils.NetworkUtils
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||||
import ac.mdiq.podcini.util.*
|
import ac.mdiq.podcini.util.*
|
||||||
|
|
|
@ -21,10 +21,10 @@
|
||||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||||
app:navigationIcon="?homeAsUpIndicator" />
|
app:navigationIcon="?homeAsUpIndicator" />
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||||
android:id="@+id/infobar"
|
<!-- android:id="@+id/infobar"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="wrap_content"/>
|
<!-- android:layout_height="wrap_content"/>-->
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,10 @@
|
||||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||||
app:navigationIcon="?homeAsUpIndicator" />
|
app:navigationIcon="?homeAsUpIndicator" />
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||||
android:id="@+id/infobar"
|
<!-- android:id="@+id/infobar"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="wrap_content"/>
|
<!-- android:layout_height="wrap_content"/>-->
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
<!-- <androidx.compose.ui.platform.ComposeView-->
|
||||||
android:id="@+id/infobar"
|
<!-- android:id="@+id/infobar"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="wrap_content"/>
|
<!-- android:layout_height="wrap_content"/>-->
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -25,232 +25,9 @@
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:id="@+id/mainView"
|
||||||
android:id="@+id/transparentBackground"
|
|
||||||
android:layout_width="match_parent"
|
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>
|
</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
|
# 6.8.7
|
||||||
|
|
||||||
* clear history really clears it
|
* clear history really clears it
|
||||||
|
@ -27,7 +44,7 @@
|
||||||
* migrated mostly the following view to Jetpack Compose:
|
* migrated mostly the following view to Jetpack Compose:
|
||||||
* Queues, AudioPlayer, Subscriptions, FeedInfo, EpisodeInfo, FeedEpisodes, AllEpisodes, History, Search, and OnlineFeed
|
* 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
|
* 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
|
* supposedly notification will not disappear and play will not stop through a playlist
|
||||||
* please voice any irregularities you may see
|
* please voice any irregularities you may see
|
||||||
* on episode lists, show duration on the top row
|
* 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