6.13.5 commit

This commit is contained in:
Xilin Jia 2024-11-05 22:33:55 +01:00
parent 2d614ad5d1
commit a0d6557de2
15 changed files with 361 additions and 259 deletions

View File

@ -12,11 +12,12 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13) [<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
#### Podcini.R 6.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts #### Podcini.R 6.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs #### Podcini.R 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
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.11.5, Podcini.R is back to be built to target SDK 35 (Android 15), but requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88) #### Since version 6.11.5, Podcini.R is back to be built to target SDK 35 (Android 15), but requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
#### If you intend to sync with through a server, be cautious as it's not well tested with Podcini. Welcome any ideas and contribution on this.
#### 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.
@ -141,6 +142,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini * Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini
* Youtube channels can be subscribed as normal podcasts * Youtube channels can be subscribed as normal podcasts
* When subscribing to a Youtube channel, tabs can be chosen to form separate podcasts
* Playlists and podcasts on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels * Playlists and podcasts on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels
* Single media from Youtube or Youtube Music can also be shared from other apps, can be accepted as including video or audio only, are added to synthetic podcasts such as "Youtube Syndicate" * Single media from Youtube or Youtube Music can also be shared from other apps, can be accepted as including video or audio only, are added to synthetic podcasts such as "Youtube Syndicate"
* 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

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = [] vectorDrawables.generatedDensities = []
versionCode 3020291 versionCode 3020292
versionName "6.13.4" versionName "6.13.5"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -18,11 +18,22 @@ import ac.mdiq.vista.extractor.InfoItem
import ac.mdiq.vista.extractor.Vista import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.channel.ChannelInfo import ac.mdiq.vista.extractor.channel.ChannelInfo
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
import ac.mdiq.vista.extractor.exceptions.ExtractionException
import ac.mdiq.vista.extractor.playlist.PlaylistInfo import ac.mdiq.vista.extractor.playlist.PlaylistInfo
import ac.mdiq.vista.extractor.stream.StreamInfoItem import ac.mdiq.vista.extractor.stream.StreamInfoItem
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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 io.realm.kotlin.ext.realmListOf import io.realm.kotlin.ext.realmListOf
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.jsoup.Jsoup import org.jsoup.Jsoup
@ -34,123 +45,195 @@ import java.net.URL
class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) { class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) {
private val TAG = "DirectSubscribe" private val TAG = "DirectSubscribe"
private val service by lazy { Vista.getService("YouTube") }
private val ytTabsMap: MutableMap<Int, String> = mutableMapOf()
private lateinit var channelInfo: ChannelInfo
var feedSource: String = "" var feedSource: String = ""
var selectedDownloadUrl: String? = null var selectedDownloadUrl: String? = null
private var downloader: Downloader? = null private var downloader: Downloader? = null
fun startFeedBuilding(url: String, username: String?, password: String?, handleFeed: (Feed, Map<String, String>)->Unit) { private var urlInit: String = ""
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
feedSource = "VistaGuide"
CoroutineScope(Dispatchers.IO).launch {
try {
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
selectedDownloadUrl = prepareUrl(url)
val feed_ = Feed(selectedDownloadUrl, null)
feed_.isBuilding = true
feed_.id = Feed.newId()
feed_.type = Feed.FeedType.YOUTUBE.name
feed_.hasVideoMedia = true
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
val eList: MutableList<Episode> = mutableListOf()
val uURL = URL(url) fun isYoutube(url: String): Boolean {
if (uURL.path.startsWith("/playlist") || uURL.path.startsWith("/playlist")) { urlInit = url
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch val isit = (feedSource == "VistaGuide" || url.contains("youtube.com"))
feed_.title = playlistInfo.name if (isit) feedSource = "VistaGuide"
feed_.description = playlistInfo.description?.content ?: "" return isit
feed_.author = playlistInfo.uploaderName }
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
feed_.episodes = realmListOf()
var infoItems = playlistInfo.relatedItems
var nextPage = playlistInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}")
CoroutineScope(Dispatchers.IO).launch {
while (infoItems.isNotEmpty()) {
eList.clear()
for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue
val e = episodeFromStreamInfoItem(r)
e.feed = feed_
e.feedId = feed_.id
eList.add(e)
}
feed_.episodes.addAll(eList)
if (nextPage == null || feed_.episodes.size > 1000) break
try {
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
nextPage = page.nextPage
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
break
}
}
feed_.isBuilding = false
}
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
} else {
val channelInfo = ChannelInfo.getInfo(service, url)
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
if (channelInfo.tabs.isEmpty()) {
withContext(Dispatchers.Main) { showError("Channel is empty", "") }
return@launch
}
try {
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
feed_.title = channelInfo.name
feed_.description = channelInfo.description
feed_.author = channelInfo.parentChannelName
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
feed_.episodes = realmListOf()
var infoItems = channelTabInfo.relatedItems fun isYoutubeChannel(): Boolean {
var nextPage = channelTabInfo.nextPage val uURL = URL(urlInit)
Logd(TAG, "infoItems: ${infoItems.size}") return !uURL.path.startsWith("/playlist")
CoroutineScope(Dispatchers.IO).launch { }
while (infoItems.isNotEmpty()) {
eList.clear() fun youtubeChannelValidTabs(): Int {
for (r in infoItems) { channelInfo = ChannelInfo.getInfo(service, urlInit)
Logd(TAG, "startFeedBuilding relatedItem: $r") var count = 0
if (r.infoType != InfoItem.InfoType.STREAM) continue for (i in channelInfo.tabs.indices) {
val e = episodeFromStreamInfoItem(r as StreamInfoItem) val t = channelInfo.tabs[i]
e.feed = feed_ var url = t.url
e.feedId = feed_.id Logd(TAG, "url: $url ${t.originalUrl} ${t.baseUrl}")
eList.add(e) if (!url.startsWith("http")) url = urlInit + url
} val uURL = URL(url)
feed_.episodes.addAll(eList) val urlEnd = uURL.path.split("/").last()
if (nextPage == null || feed_.episodes.size > 1000) break if (urlEnd != "playlists" && urlEnd != "shorts") count++
try { }
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage) return count
nextPage = page.nextPage }
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}") @Composable
} catch (e: Throwable) { fun ConfirmYTChannelTabsDialog(onDismissRequest: () -> Unit, handleFeed: (Feed, Map<String, String>)->Unit) {
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}") val textColor = MaterialTheme.colorScheme.onSurface
withContext(Dispatchers.Main) { showError(e.message, "") } Column {
break Text(text = stringResource(R.string.choose_tab), color = textColor, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp, bottom = 10.dp))
} val selectedId = remember { mutableStateOf<Int?>(null) }
} for (i in channelInfo.tabs.indices) {
feed_.isBuilding = false val t = channelInfo.tabs[i]
} var url = t.url
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) } Logd(TAG, "url: $url ${t.originalUrl} ${t.baseUrl}")
} catch (e: Throwable) { if (!url.startsWith("http")) url = urlInit + url
Logd(TAG, "startFeedBuilding error1 ${e.message}") val uURL = URL(url)
withContext(Dispatchers.Main) { showError(e.message, "") } val urlEnd = uURL.path.split("/").last()
} if (urlEnd != "playlists" && urlEnd != "shorts") Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 30.dp)) {
var checked by remember { mutableStateOf(false) }
Checkbox(checked = selectedId.value == i, onCheckedChange = {
selectedId.value = if (selectedId.value == i) null else i
checked = it
if (checked) ytTabsMap[i] = urlEnd else ytTabsMap.remove(i)
})
Text(text = urlEnd, color = textColor, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
} }
} catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
} }
} }
return Button(modifier = Modifier.padding(start = 10.dp, top = 10.dp), onClick = {
CoroutineScope(Dispatchers.IO).launch {
for (i in ytTabsMap.keys) {
Logd(TAG, "Subscribing $i ${channelInfo.tabs[i].url}")
buildYTChannel(i, ytTabsMap[i]!!) { feed_, map -> handleFeed(feed_, map) }
}
}
onDismissRequest()
}) {
Text("Confirm")
}
} }
}
// handle normal podcast source suspend fun buildYTPlaylist(handleFeed: (Feed, Map<String, String>)->Unit) {
try {
val url = urlInit
val playlistInfo = PlaylistInfo.getInfo(service, url) ?: return
selectedDownloadUrl = prepareUrl(url)
Logd(TAG, "selectedDownloadUrl: $selectedDownloadUrl url: $url")
val feed_ = Feed(selectedDownloadUrl, null)
feed_.isBuilding = true
feed_.id = Feed.newId()
feed_.type = Feed.FeedType.YOUTUBE.name
feed_.hasVideoMedia = true
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
val eList: MutableList<Episode> = mutableListOf()
feed_.title = playlistInfo.name
feed_.description = playlistInfo.description?.content ?: ""
feed_.author = playlistInfo.uploaderName
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
feed_.episodes = realmListOf()
var infoItems = playlistInfo.relatedItems
var nextPage = playlistInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}")
CoroutineScope(Dispatchers.IO).launch {
while (infoItems.isNotEmpty()) {
eList.clear()
for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue
val e = episodeFromStreamInfoItem(r)
e.feed = feed_
e.feedId = feed_.id
eList.add(e)
}
feed_.episodes.addAll(eList)
if (nextPage == null || feed_.episodes.size > 2000) break
try {
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
nextPage = page.nextPage
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
break
}
}
feed_.isBuilding = false
}
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
} catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
}
}
suspend fun buildYTChannel(index: Int, title: String, handleFeed: (Feed, Map<String, String>)->Unit) {
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
var url = channelInfo.tabs[index].url
if (!url.startsWith("http")) url = urlInit
try {
selectedDownloadUrl = prepareUrl(url)
Logd(TAG, "selectedDownloadUrl: $selectedDownloadUrl url: $url")
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs[index])
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
val feed_ = Feed(selectedDownloadUrl, null)
feed_.isBuilding = true
feed_.id = Feed.newId()
feed_.type = Feed.FeedType.YOUTUBE.name
feed_.hasVideoMedia = true
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
val eList: MutableList<Episode> = mutableListOf()
feed_.title = channelInfo.name + " " + title
feed_.description = channelInfo.description
feed_.author = channelInfo.parentChannelName
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
feed_.episodes = realmListOf()
var infoItems = channelTabInfo.relatedItems
var nextPage = channelTabInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}")
CoroutineScope(Dispatchers.IO).launch {
while (infoItems.isNotEmpty()) {
eList.clear()
for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
e.feed = feed_
e.feedId = feed_.id
eList.add(e)
}
feed_.episodes.addAll(eList)
if (nextPage == null || feed_.episodes.size > 2000) break
try {
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
nextPage = page.nextPage
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
break
}
}
feed_.isBuilding = false
}
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
} catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error1 ${e.message}")
withContext(Dispatchers.Main) { showError(e.message, "") }
}
}
fun buildPodcast(url: String, username: String?, password: String?, handleFeed: (Feed, Map<String, String>)->Unit) {
when (val urlType = htmlOrXml(url)) { when (val urlType = htmlOrXml(url)) {
"HTML" -> { "HTML" -> {
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
@ -159,7 +242,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
for (element in linkElements) { for (element in linkElements) {
val rssUrl = element.attr("href") val rssUrl = element.attr("href")
Logd(TAG, "RSS URL: $rssUrl") Logd(TAG, "RSS URL: $rssUrl")
startFeedBuilding(rssUrl, username, password) {feed, map -> handleFeed(feed, map) } buildPodcast(rssUrl, username, password) {feed, map -> handleFeed(feed, map) }
} }
} }
"XML" -> {} "XML" -> {}
@ -280,5 +363,4 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
// } // }
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}") Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")
} }
} }

View File

@ -31,6 +31,7 @@ import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.channel.ChannelInfo import ac.mdiq.vista.extractor.channel.ChannelInfo
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
import ac.mdiq.vista.extractor.exceptions.ExtractionException import ac.mdiq.vista.extractor.exceptions.ExtractionException
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
import ac.mdiq.vista.extractor.stream.StreamInfoItem import ac.mdiq.vista.extractor.stream.StreamInfoItem
import android.Manifest import android.Manifest
import android.app.Notification import android.app.Notification
@ -52,6 +53,7 @@ import io.realm.kotlin.types.RealmList
import org.xml.sax.SAXException import org.xml.sax.SAXException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -233,29 +235,50 @@ object FeedUpdateManager {
} }
} }
private fun refreshYoutubeFeed(feed: Feed) { private fun refreshYoutubeFeed(feed: Feed) {
if (feed.downloadUrl.isNullOrEmpty()) return
val url = feed.downloadUrl
try { try {
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") } val service = try { Vista.getService("YouTube")
val channelInfo = ChannelInfo.getInfo(service, feed.downloadUrl!!) } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
if (channelInfo.tabs.isEmpty()) return val uURL = URL(url)
try { if (uURL.path.startsWith("/channel")) {
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first()) val channelInfo = ChannelInfo.getInfo(service, url!!)
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}") Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
if (channelInfo.tabs.isEmpty()) return
try {
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
val eList: RealmList<Episode> = realmListOf()
for (r in channelTabInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r as StreamInfoItem))
val feed_ = Feed(url, null)
feed_.type = Feed.FeedType.YOUTUBE.name
feed_.hasVideoMedia = true
feed_.title = channelInfo.name
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
feed_.description = channelInfo.description
feed_.author = channelInfo.parentChannelName
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
feed_.episodes = eList
Feeds.updateFeed(applicationContext, feed_, false)
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed channel error1 ${e.message}") }
} else if (uURL.path.startsWith("/playlist")) {
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return
val eList: RealmList<Episode> = realmListOf() val eList: RealmList<Episode> = realmListOf()
for (r in channelTabInfo.relatedItems) { try {
eList.add(episodeFromStreamInfoItem(r as StreamInfoItem)) for (r in playlistInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r))
} val feed_ = Feed(url, null)
val feed_ = Feed(feed.downloadUrl, null) feed_.type = Feed.FeedType.YOUTUBE.name
feed_.type = Feed.FeedType.YOUTUBE.name feed_.hasVideoMedia = true
feed_.hasVideoMedia = true feed_.title = playlistInfo.name
feed_.title = channelInfo.name feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString() feed_.description = playlistInfo.description?.content ?: ""
feed_.description = channelInfo.description feed_.author = playlistInfo.uploaderName
feed_.author = channelInfo.parentChannelName feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null feed_.episodes = eList
feed_.episodes = eList Feeds.updateFeed(applicationContext, feed_, false)
Feeds.updateFeed(applicationContext, feed_, false) } catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed playlist error1 ${e.message}") }
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error1 ${e.message}") } }
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error ${e.message}") } } catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error ${e.message}") }
} }

View File

@ -36,7 +36,6 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -60,8 +59,6 @@ import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -69,7 +66,6 @@ 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.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View File

@ -986,9 +986,7 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
} }
withContext(Dispatchers.Main) { onDismissRequest() } withContext(Dispatchers.Main) { onDismissRequest() }
} }
}) { }) { Text("Confirm") }
Text("Confirm")
}
} else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp)) } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
} }
} }

View File

@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.compose
import ac.mdiq.podcini.R 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.storage.database.Feeds
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
@ -55,10 +54,7 @@ fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) { for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
for (item in selected) { for (item in selected) upsertBlk(item) { it.rating = rating.code }
// Feeds.setRating(item, rating.code)
upsertBlk(item) { it.rating = rating.code }
}
onDismissRequest() onDismissRequest()
}) { }) {
Icon(imageVector = ImageVector.vectorResource(id = rating.res), "") Icon(imageVector = ImageVector.vectorResource(id = rating.res), "")
@ -85,8 +81,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message) Text(message)
Text(stringResource(R.string.feed_delete_reason_msg)) Text(stringResource(R.string.feed_delete_reason_msg))
BasicTextField(value = textState, onValueChange = { textState = it }, BasicTextField(value = textState, onValueChange = { textState = it }, textStyle = TextStyle(fontSize = 16.sp, color = textColor),
textStyle = TextStyle(fontSize = 16.sp, color = textColor),
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small) modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
) )
Button(onClick = { Button(onClick = {
@ -119,9 +114,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
} catch (e: Throwable) { Log.e("RemoveFeedDialog", Log.getStackTraceString(e)) } } catch (e: Throwable) { Log.e("RemoveFeedDialog", Log.getStackTraceString(e)) }
} }
onDismissRequest() onDismissRequest()
}) { }) { Text("Confirm") }
Text("Confirm")
}
} }
} }
} }
@ -130,41 +123,43 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: SubscriptionLog? = null) { fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: SubscriptionLog? = null) {
// var showYTChannelDialog by remember { mutableStateOf(false) }
// if (showYTChannelDialog) feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = {showYTChannelDialog = false}) {feed, map -> handleFeed(feed, map)}
val showSubscribeDialog = remember { mutableStateOf(false) } val showSubscribeDialog = remember { mutableStateOf(false) }
@Composable @Composable
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(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
shape = RoundedCornerShape(16.dp)) { val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
Text("Subscribe: \"${feed.title}\" ?") Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp))
Button(onClick = { Button(onClick = {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (feed.feedUrl != null) { if (feed.feedUrl != null) {
val feedBuilder = FeedBuilder(activity) { message, details -> val feedBuilder = FeedBuilder(activity) { message, details -> Logd("OnineFeedItem", "Subscribe error: $message \n $details") }
Logd("OnineFeedItem", "Subscribe error: $message \n $details")
}
feedBuilder.feedSource = feed.source feedBuilder.feedSource = feed.source
feedBuilder.startFeedBuilding(feed.feedUrl, val url = feed.feedUrl
"", if (feedBuilder.isYoutube(url)) {
"") { feed, _ -> feedBuilder.subscribe(feed) } if (feedBuilder.isYoutubeChannel()) {
val nTabs = feedBuilder.youtubeChannelValidTabs()
feedBuilder.buildYTChannel(0, "") { feed, _ -> feedBuilder.subscribe(feed) }
// if (nTabs > 1) showYTChannelDialog = true
// else feedBuilder.buildYTChannel(0, "") { feed, map -> feedBuilder.subscribe(feed) }
} else feedBuilder.buildYTPlaylist { feed, _ -> feedBuilder.subscribe(feed) }
} else feedBuilder.buildPodcast(url, "", "") { feed, _ -> feedBuilder.subscribe(feed) }
} }
} }
onDismissRequest() onDismissRequest()
}) { }) { Text("Confirm") }
Text("Confirm")
}
} }
} }
} }
} }
} }
if (showSubscribeDialog.value) { if (showSubscribeDialog.value) confirmSubscribe(feed, showSubscribeDialog.value, onDismissRequest = { showSubscribeDialog.value = false })
confirmSubscribe(feed, showSubscribeDialog.value, onDismissRequest = {
showSubscribeDialog.value = false
})
}
val context = LocalContext.current val context = LocalContext.current
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 = {
@ -180,11 +175,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
} }
}, onLongClick = { showSubscribeDialog.value = true })) { }, onLongClick = { showSubscribeDialog.value = true })) {
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Text(feed.title, Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
color = textColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(bottom = 4.dp))
Row { Row {
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()
@ -196,7 +187,6 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}) })
if (feed.feedId > 0 || log != null) { if (feed.feedId > 0 || log != null) {
Logd("OnlineFeedItem", "${feed.feedId} $log") Logd("OnlineFeedItem", "${feed.feedId} $log")
val alpha = 1.0f val alpha = 1.0f
@ -215,26 +205,14 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
feed.feedUrl != null && !feed.feedUrl.contains("itunes.apple.com") -> feed.feedUrl feed.feedUrl != null && !feed.feedUrl.contains("itunes.apple.com") -> feed.feedUrl
else -> "" else -> ""
} }
if (authorText.isNotEmpty()) Text(authorText, if (authorText.isNotEmpty()) Text(authorText, color = textColor, style = MaterialTheme.typography.bodyMedium)
color = textColor, if (feed.subscriberCount > 0) Text(MiscFormatter.formatNumber(feed.subscriberCount) + " subscribers", color = textColor, style = MaterialTheme.typography.bodyMedium)
style = MaterialTheme.typography.bodyMedium)
if (feed.subscriberCount > 0) Text(MiscFormatter.formatNumber(feed.subscriberCount) + " subscribers",
color = textColor,
style = MaterialTheme.typography.bodyMedium)
Row { Row {
if (feed.count != null && feed.count > 0) Text(feed.count.toString() + " episodes", if (feed.count != null && feed.count > 0) Text(feed.count.toString() + " episodes", color = textColor, style = MaterialTheme.typography.bodyMedium)
color = textColor,
style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
if (feed.update != null) Text(feed.update, if (feed.update != null) Text(feed.update, color = textColor, style = MaterialTheme.typography.bodyMedium)
color = textColor,
style = MaterialTheme.typography.bodyMedium)
} }
Text(feed.source + ": " + feed.feedUrl, Text(feed.source + ": " + feed.feedUrl, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall)
color = textColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.labelSmall)
} }
} }
} }

View File

@ -77,7 +77,6 @@ import kotlin.concurrent.Volatile
* If the feed cannot be downloaded or parsed, an error dialog will be displayed * If the feed cannot be downloaded or parsed, an error dialog will be displayed
* and the activity will finish as soon as the error dialog is closed. * and the activity will finish as soon as the error dialog is closed.
*/ */
class OnlineFeedFragment : Fragment() { class OnlineFeedFragment : Fragment() {
private var _binding: ComposeFragmentBinding? = null private var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -86,7 +85,9 @@ class OnlineFeedFragment : Fragment() {
var feedSource: String = "" var feedSource: String = ""
private var feedUrl: String = "" private var feedUrl: String = ""
private var urlToLog: String = ""
private lateinit var feedBuilder: FeedBuilder private lateinit var feedBuilder: FeedBuilder
private var showYTChannelDialog by mutableStateOf(false)
private var isShared: Boolean = false private var isShared: Boolean = false
@ -134,6 +135,7 @@ class OnlineFeedFragment : Fragment() {
binding.mainView.setContent { binding.mainView.setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
if (showYTChannelDialog) feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = {showYTChannelDialog = false}) {feed, map -> handleFeed(feed, map)}
MainView() MainView()
} }
} }
@ -181,23 +183,32 @@ class OnlineFeedFragment : Fragment() {
outState.putString("password", password) outState.putString("password", password)
} }
private fun handleFeed(feed_: Feed, map: Map<String, String>) {
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
feed = feed_
if (isShared) {
val log = realm.query(ShareLog::class).query("url == $0", urlToLog).first().find()
if (log != null) upsertBlk(log) {
it.title = feed_.title
it.author = feed_.author
}
}
showFeedInformation(feed_, map)
}
private fun lookupUrlAndBuild(url: String) { private fun lookupUrlAndBuild(url: String) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
urlToLog = url
val urlString = PodcastSearcherRegistry.lookupUrl1(url) val urlString = PodcastSearcherRegistry.lookupUrl1(url)
try { try {
feeds = getFeedList() feeds = getFeedList()
feedBuilder.startFeedBuilding(urlString, username, password) { feed_, map -> if (feedBuilder.isYoutube(urlString)) {
selectedDownloadUrl = feedBuilder.selectedDownloadUrl if (feedBuilder.isYoutubeChannel()) {
feed = feed_ val nTabs = feedBuilder.youtubeChannelValidTabs()
if (isShared) { if (nTabs > 1) showYTChannelDialog = true
val log = realm.query(ShareLog::class).query("url == $0", url).first().find() else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) }
if (log != null) upsertBlk(log) { } else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) }
it.title = feed_.title } else feedBuilder.buildPodcast(urlString, username, password) { feed_, map -> handleFeed(feed_, map) }
it.author = feed_.author
}
}
showFeedInformation(feed_, map)
}
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e) } catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e)) Log.e(TAG, Log.getStackTraceString(e))
@ -223,21 +234,17 @@ class OnlineFeedFragment : Fragment() {
} }
} }
if (url != null) { if (url != null) {
urlToLog = url
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 -> if (feedBuilder.isYoutube(url)) {
selectedDownloadUrl = feedBuilder.selectedDownloadUrl if (feedBuilder.isYoutubeChannel()) {
feed = feed_ val nTabs = feedBuilder.youtubeChannelValidTabs()
if (isShared) { if (nTabs > 1) showYTChannelDialog = true
val log = realm.query(ShareLog::class).query("url == $0", url).first().find() else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) }
if (log != null) upsertBlk(log) { } else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) }
it.title = feed_.title } else feedBuilder.buildPodcast(url, username, password) { feed_, map -> handleFeed(feed_, map) }
it.author = feed_.author
}
}
showFeedInformation(feed_, map)
}
} else { } else {
showNoPodcastFoundError() showNoPodcastFoundError()
Logd(TAG, "Failed to retrieve feed url") Logd(TAG, "Failed to retrieve feed url")
@ -352,20 +359,19 @@ class OnlineFeedFragment : Fragment() {
val (progressBar, main) = createRefs() val (progressBar, main) = createRefs()
if (showProgress) CircularProgressIndicator(progress = { 0.6f }, if (showProgress) CircularProgressIndicator(progress = { 0.6f },
strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) }) 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) else Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp).constrainAs(main) {
.constrainAs(main) { top.linkTo(parent.top)
top.linkTo(parent.top) bottom.linkTo(parent.bottom)
bottom.linkTo(parent.bottom) start.linkTo(parent.start)
start.linkTo(parent.start) }) {
}) {
if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) { if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) {
val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs() val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs()
if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), contentDescription = "background", // if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), contentDescription = "background",
Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) { // Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) {
top.linkTo(parent.top) // top.linkTo(parent.top)
bottom.linkTo(parent.bottom) // bottom.linkTo(parent.bottom)
start.linkTo(parent.start) // start.linkTo(parent.start)
}) // })
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher), AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) { modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
@ -387,9 +393,7 @@ class OnlineFeedFragment : Fragment() {
if (feedInFeedlist() || isSubscribed(feed!!)) { if (feedInFeedlist() || isSubscribed(feed!!)) {
if (isShared) { if (isShared) {
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
if (log != null) upsertBlk(log) { if (log != null) upsertBlk(log) { it.status = ShareLog.Status.EXISTING.ordinal }
it.status = ShareLog.Status.EXISTING.ordinal
}
} }
val feed = getFeedByTitleAndAuthor(feed?.eigenTitle?:"", feed?.author?:"") val feed = getFeedByTitleAndAuthor(feed?.eigenTitle?:"", feed?.author?:"")
if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
@ -402,9 +406,7 @@ class OnlineFeedFragment : Fragment() {
feedBuilder.subscribe(feed!!) feedBuilder.subscribe(feed!!)
if (isShared) { if (isShared) {
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
if (log != null) upsertBlk(log) { if (log != null) upsertBlk(log) { it.status = ShareLog.Status.SUCCESS.ordinal }
it.status = ShareLog.Status.SUCCESS.ordinal
}
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
enableSubscribe = true enableSubscribe = true
@ -413,20 +415,16 @@ class OnlineFeedFragment : Fragment() {
} }
} }
} }
}) { }) { Text(stringResource(subButTextRes)) }
Text(stringResource(subButTextRes))
}
Spacer(modifier = Modifier.weight(0.1f)) Spacer(modifier = Modifier.weight(0.1f))
if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) { if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) { Text(stringResource(R.string.episodes_label)) }
Text(stringResource(R.string.episodes_label))
}
Spacer(modifier = Modifier.weight(0.2f)) Spacer(modifier = Modifier.weight(0.2f))
} }
if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier // if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier
.constrainAs(closeButton) { // .constrainAs(closeButton) {
top.linkTo(parent.top) // top.linkTo(parent.top)
end.linkTo(parent.end) // end.linkTo(parent.end)
}) // })
} }
Column { Column {
// alternate_urls_spinner // alternate_urls_spinner
@ -706,7 +704,6 @@ class OnlineFeedFragment : Fragment() {
} }
} }
class RemoteEpisodesFragment : BaseEpisodesFragment() { class RemoteEpisodesFragment : BaseEpisodesFragment() {
private val episodeList: MutableList<Episode> = mutableListOf() private val episodeList: MutableList<Episode> = mutableListOf()

View File

@ -148,13 +148,15 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
spinnerView = ComposeView(requireContext()).apply { spinnerView = ComposeView(requireContext()).apply {
setContent { setContent {
Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index:Int -> CustomTheme(requireContext()) {
Logd(TAG, "Queue selected: $queues[index].name") Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index: Int ->
val prevQueueSize = curQueue.size() Logd(TAG, "Queue selected: $queues[index].name")
curQueue = upsertBlk(queues[index]) { it.update() } val prevQueueSize = curQueue.size()
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") curQueue = upsertBlk(queues[index]) { it.update() }
loadCurQueue(true) toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
playbackService?.notifyCurQueueItemsChanged(max(prevQueueSize, curQueue.size())) loadCurQueue(true)
playbackService?.notifyCurQueueItemsChanged(max(prevQueueSize, curQueue.size()))
}
} }
} }
} }

View File

@ -65,7 +65,6 @@ import java.text.NumberFormat
/** /**
* Performs a search operation on all feeds or one specific feed and displays the search result. * Performs a search operation on all feeds or one specific feed and displays the search result.
*/ */
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
private var _binding: SearchFragmentBinding? = null private var _binding: SearchFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -120,7 +119,6 @@ class SearchFragment : Fragment() {
} }
refreshSwipeTelltale() refreshSwipeTelltale()
chip = binding.feedTitleChip chip = binding.feedTitleChip
chip.setOnCloseIconClickListener { chip.setOnCloseIconClickListener {
requireArguments().putLong(ARG_FEED, 0) requireArguments().putLong(ARG_FEED, 0)

View File

@ -54,7 +54,8 @@ class FeedStatisticsFragment : Fragment() {
} }
private fun showStats(s: StatisticsItem?) { private fun showStats(s: StatisticsItem?) {
binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s!!.episodesStarted, s.numEpisodes) if (s == null) return
binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s.episodesStarted, s.numEpisodes)
binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed) binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed)
binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time) binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time)
binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount) binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount)

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M8,8H6v7c0,1.1 0.9,2 2,2h9v-2H8V8z"/> <path android:fillColor="?attr/action_icon_color" android:pathData="M8,8H6v7c0,1.1 0.9,2 2,2h9v-2H8V8z"/>
<path android:fillColor="@android:color/white" android:pathData="M20,3h-8c-1.1,0 -2,0.9 -2,2v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM20,11h-8V7h8V11z"/> <path android:fillColor="?attr/action_icon_color" android:pathData="M20,3h-8c-1.1,0 -2,0.9 -2,2v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM20,11h-8V7h8V11z"/>
<path android:fillColor="@android:color/white" android:pathData="M4,12H2v7c0,1.1 0.9,2 2,2h9v-2H4V12z"/> <path android:fillColor="?attr/action_icon_color" android:pathData="M4,12H2v7c0,1.1 0.9,2 2,2h9v-2H4V12z"/>
</vector> </vector>

View File

@ -276,6 +276,8 @@
<string name="add_opinion_label">Add opinion</string> <string name="add_opinion_label">Add opinion</string>
<string name="cancelled_on_label">Cancelled on</string> <string name="cancelled_on_label">Cancelled on</string>
<string name="choose_tab">Choose a tab in the channel</string>
<string name="set_play_state_label">Set played state</string> <string name="set_play_state_label">Set played state</string>
<string name="mark_read_no_media_label">Mark as read</string> <string name="mark_read_no_media_label">Mark as read</string>

View File

@ -1,3 +1,15 @@
# 6.13.5
* hopefully fixed youtube playlists/podcasts no-updating issue
* in OnlineFeed, when subscribing to a Youtube channel, a popup allows to choose a tab (if multiples are available)
* tab options include Videos (or customized name, the default tab) and Streams (live tab)
* only one tab can be chosen at a time
* the tab name will be appended to the feed title
* each tab results in a feed/podcast in Podcini
* the shortcut way of subscribing (long-click on a search result) only subscribes to the default tab
* episodes limits on subscribing to YT channels/playlists/podcasts are raised to 2000
* fixed toolbar contrasts on Queues view
# 6.13.4 # 6.13.4
* in Queues view, reworked the spinner in Compose and added associated feeds toggle * in Queues view, reworked the spinner in Compose and added associated feeds toggle

View File

@ -0,0 +1,11 @@
Version 6.13.5
* hopefully fixed youtube playlists/podcasts no-updating issue
* in OnlineFeed, when subscribing to a Youtube channel, a popup allows to choose a tab (if multiples are available)
* tab options include Videos (or customized name, the default tab) and Streams (live tab)
* only one tab can be chosen at a time
* the tab name will be appended to the feed title
* each tab results in a feed/podcast in Podcini
* the shortcut way of subscribing (long-click on a search result) only subscribes to the default tab
* episodes limits on subscribing to YT channels/playlists/podcasts are raised to 2000
* fixed toolbar contrasts on Queues view