diff --git a/README.md b/README.md
index 8f536f88..7da87559 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[](https://f-droid.org/packages/ac.mdiq.podcini.R/)
[](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
+#### Podcini.R 6.6 is capable of receiving/handling shared single media from Youtube, for more see the changelogs.
#### Podcini.R version 6.5 as a major step forward brings YouTube channels in the app. They can be searched, subscribed and played from within Podcini. For more see the changelogs
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
@@ -27,7 +28,7 @@ Compared to AntennaPod this project:
5. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
6. Supports multiple, virtual and circular play queues associable to any podcast
7. Auto-download is governed by policy and limit settings of individual feed
-8. Accepts podcast as well as Youtube channels and plain RSS,
+8. Accepts podcast, Youtube channels, Youtube media and plain RSS,
9. Offers Readability and Text-to-Speech for RSS contents,
10. Features `instant sync` across devices without a server.
@@ -124,7 +125,15 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Ability to open podcast from webpage address
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes
* Online feed episodes can be freely played (streamed) without a subscription
-* Youtube channels can be searched in podcast search view, and can be subscribed as a normal podcast.
+
+### Youtube channels and media
+
+* 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
+* Single Youtube media can also be shared from other apps, once received, are added to artificial podcast "Youtube Syndicate"
+* All Youtube media can be played (only streamed) with video in fullscreen and in window as well as in audio only mode
+* Every Youtube media comes with the lowest video quality and highest audio quality
+* If a Youtube channel podcast is set for "audio only", then only audio stream is fetched at play time for every media in the podcast
### Instant (or Wifi) sync
diff --git a/app/build.gradle b/app/build.gradle
index 55afe041..7ff0e372 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- versionCode 3020244
- versionName "6.5.10"
+ versionCode 3020245
+ versionName "6.6.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@@ -172,17 +172,14 @@ android {
dependencies {
/** Desugaring for using VistaGuide **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
-
- def composeBom = platform('androidx.compose:compose-bom:2024.09.00')
- implementation composeBom
- androidTestImplementation composeBom
-
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
- implementation 'androidx.compose.material:material:1.7.0'
-
- implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
- debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
+ def composeBom = platform('androidx.compose:compose-bom:2024.09.01')
+ implementation composeBom
+ androidTestImplementation composeBom
+ implementation 'androidx.compose.material:material:1.7.1'
+ implementation 'androidx.compose.ui:ui-tooling-preview:1.7.1'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.7.1'
implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.window:window:1.3.0'
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
index 1257a49e..3e028156 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
@@ -69,9 +69,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
protected open fun setPlayable(playable: Playable?) {
- if (playable != null && playable !== curMedia) {
- curMedia = playable
- }
+ if (playable != null && playable !== curMedia) curMedia = playable
}
open fun getVideoSize(): Pair? {
@@ -92,7 +90,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
return -1
}
- abstract fun createMediaPlayer()
+ open fun createMediaPlayer() {}
/**
* Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
@@ -165,10 +163,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
*/
fun seekDelta(delta: Int) {
val curPosition = getPosition()
- if (curPosition != Playable.INVALID_TIME) {
- val prevMedia = curMedia
- seekTo(curPosition + delta)
- }
+ if (curPosition != Playable.INVALID_TIME) seekTo(curPosition + delta)
else Log.e(TAG, "seekDelta getPosition() returned INVALID_TIME in seekDelta")
}
@@ -309,7 +304,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
@JvmField
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
- val audioPlaybackSpeed: Float
+ val prefPlaybackSpeed: Float
get() {
try { return appPrefs.getString(Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
@@ -374,7 +369,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
}
}
- if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = audioPlaybackSpeed
+ if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = prefPlaybackSpeed
return playbackSpeed
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
index b9a301ca..3a4f02db 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
@@ -804,7 +804,6 @@ class PlaybackService : MediaLibraryService() {
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
}
val playable = curMedia
-
Log.d(TAG, "onStartCommand flags=$flags startId=$startId keycode=$keycode keyEvent=$keyEvent customAction=$customAction hardwareButton=$hardwareButton action=${intent?.action.toString()} ${playable?.getEpisodeTitle()}")
if (keycode == -1 && playable == null && customAction == null) {
Log.e(TAG, "onStartCommand PlaybackService was started with no arguments, return")
@@ -819,7 +818,6 @@ class PlaybackService : MediaLibraryService() {
Logd(TAG, "onStartCommand playing same media: $status, return")
return super.onStartCommand(intent, flags, startId)
}
-
when {
keycode != -1 -> {
Logd(TAG, "onStartCommand Received hardware button event: $hardwareButton")
@@ -1437,8 +1435,8 @@ class PlaybackService : MediaLibraryService() {
val audioIndex = if (isNetworkRestricted) 0 else audioStreamsList.size - 1
val audioStream = audioStreamsList[audioIndex]
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
- val aSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(
- Uri.parse(audioStream.content)).build())
+ val aSource = DefaultMediaSourceFactory(context).createMediaSource(
+ MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
if (media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
Logd(TAG, "setDataSource1 result: $streamInfo")
Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}")
@@ -1446,8 +1444,8 @@ class PlaybackService : MediaLibraryService() {
val videoIndex = 0
val videoStream = videoStreamsList[videoIndex]
Logd(TAG, "setDataSource1 use video quality: ${videoStream.resolution}")
- val vSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(
- Uri.parse(videoStream.content)).build())
+ val vSource = DefaultMediaSourceFactory(context).createMediaSource(
+ MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(videoStream.content)).build())
val mediaSources: MutableList = ArrayList()
mediaSources.add(vSource)
mediaSources.add(aSource)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
index cd28a176..b8f13cd0 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt
@@ -286,6 +286,7 @@ object UserPreferences {
prefDrawerFeedOrder,
prefDrawerFeedOrderDir,
prefFeedGridLayout,
+ prefSwipeToRefreshAll,
prefExpandNotify,
prefEpisodeCover,
showTimeLeft,
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
index 928aa03b..cc29b4ba 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
@@ -29,6 +29,7 @@ import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
+import ac.mdiq.vista.extractor.stream.StreamInfo
import ac.mdiq.vista.extractor.stream.StreamInfoItem
import android.app.backup.BackupManager
import android.content.Context
@@ -197,39 +198,24 @@ object Episodes {
}
}
if (removedFromQueue.isNotEmpty()) removeFromAllQueuesSync(*removedFromQueue.toTypedArray())
-
for (episode in removedFromQueue) EventFlow.postEvent(FlowEvent.QueueEvent.irreversibleRemoved(episode))
// we assume we also removed download log entries for the feed or its media files.
// especially important if download or refresh failed, as the user should not be able
// to retry these
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
-
val backupManager = BackupManager(context)
backupManager.dataChanged()
}
}
-// fun persistEpisodes(episodes: List) : Job {
-// Logd(TAG, "persistEpisodes called")
-// return runOnIOScope {
-// for (episode in episodes) {
-// Logd(TAG, "persistEpisodes: ${episode.playState} ${episode.title}")
-// upsert(episode) {}
-// }
-// EventFlow.postEvent(FlowEvent.EpisodeEvent(episodes))
-// }
-// }
-
// only used in tests
fun persistEpisodeMedia(media: EpisodeMedia) : Job {
Logd(TAG, "persistEpisodeMedia called")
return runOnIOScope {
var episode = media.episodeOrFetch()
if (episode != null) {
- episode = upsert(episode) {
- it.media = media
- }
+ episode = upsert(episode) { it.media = media }
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode))
} else Log.e(TAG, "persistEpisodeMedia media.episode is null")
}
@@ -255,9 +241,7 @@ object Episodes {
fun addToHistory(episode: Episode, date: Date? = Date()) : Job {
Logd(TAG, "addToHistory called")
return runOnIOScope {
- upsert(episode) {
- it.media?.playbackCompletionDate = date
- }
+ upsert(episode) { it.media?.playbackCompletionDate = date }
EventFlow.postEvent(FlowEvent.HistoryEvent())
}
}
@@ -266,9 +250,7 @@ object Episodes {
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
Logd(TAG, "setFavorite called $stat")
return runOnIOScope {
- val result = upsert(episode) {
- it.isFavorite = stat ?: !it.isFavorite
- }
+ val result = upsert(episode) { it.isFavorite = stat ?: !it.isFavorite }
EventFlow.postEvent(FlowEvent.FavoritesEvent(result))
}
}
@@ -309,11 +291,25 @@ object Episodes {
return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
}
- fun episodeFromStreamInfoItem(info: StreamInfoItem): Episode {
+ fun episodeFromStreamInfoItem(item: StreamInfoItem): Episode {
+ val e = Episode()
+ e.link = item.url
+ e.title = item.name
+ e.description = item.shortDescription
+ e.imageUrl = item.thumbnails.first().url
+ e.setPubDate(item.uploadDate?.date()?.time)
+ val m = EpisodeMedia(e, item.url, 0, "video/*")
+ if (item.duration > 0) m.duration = item.duration.toInt() * 1000
+ m.fileUrl = getMediafilename(m)
+ e.media = m
+ return e
+ }
+
+ fun episodeFromStreamInfo(info: StreamInfo): Episode {
val e = Episode()
e.link = info.url
e.title = info.name
- e.description = info.shortDescription
+ e.description = info.description?.content
e.imageUrl = info.thumbnails.first().url
e.setPubDate(info.uploadDate?.date()?.time)
val m = EpisodeMedia(e, info.url, 0, "video/*")
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
index 1c32bcfa..77efd6e9 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
@@ -5,9 +5,11 @@ import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
+import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
+import ac.mdiq.podcini.storage.database.Feeds.EpisodeAssistant.searchEpisodeByIdentifyingValue
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
@@ -19,6 +21,8 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting
+import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
+import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@@ -412,6 +416,43 @@ object Feeds {
return !feed.isLocalFeed || isAutoDeleteLocal
}
+ fun getYoutubeSyndicate(video: Boolean): Feed {
+ val feedId: Long = if (video) 1 else 2
+ var feed = getFeed(feedId, true)
+ if (feed != null) return feed
+
+ feed = Feed()
+ feed.id = feedId
+ feed.title = "Youtube Syndicate" + if (video) "" else " Audio"
+ feed.type = Feed.FeedType.YOUTUBE.name
+ feed.hasVideoMedia = video
+ feed.downloadUrl = null
+ feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString()
+ feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
+ feed.preferences!!.keepUpdated = false
+ feed.preferences!!.queue = null
+ feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
+ upsertBlk(feed) {}
+ return feed
+ }
+
+ fun addToYoutubeSyndicate(episode: Episode, video: Boolean) {
+ val feed = getYoutubeSyndicate(video)
+ Logd(TAG, "addToYoutubeSyndicate: feed: ${feed.title}")
+ if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return
+
+ Logd(TAG, "addToYoutubeSyndicate adding new episode: ${episode.title}")
+ runOnIOScope {
+ episode.feed = feed
+ episode.id = Feed.newId()
+ episode.feedId = feed.id
+ episode.media?.id = episode.id
+ upsert(episode) {}
+ feed.episodes.add(episode)
+ upsert(feed) {}
+ }
+ }
+
/**
* Compares the pubDate of two FeedItems for sorting in reverse order
*/
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt
index b7cbb0b5..782db8ee 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt
@@ -3,11 +3,8 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
-import ac.mdiq.podcini.storage.model.RemoteMedia.Companion.PLAYABLE_TYPE_REMOTE_MEDIA
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.util.Logd
-import ac.mdiq.vista.extractor.Vista
-import ac.mdiq.vista.extractor.stream.StreamInfo
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
index cb0d95ce..4932fb83 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
@@ -1,20 +1,41 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
+import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo
+import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate
+import ac.mdiq.podcini.storage.model.Episode
+import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.Logd
+import ac.mdiq.vista.extractor.Vista
+import ac.mdiq.vista.extractor.stream.StreamInfo
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
+import android.view.ViewGroup
+import androidx.activity.compose.setContent
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+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.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.net.URLDecoder
class ShareReceiverActivity : AppCompatActivity() {
-
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -24,24 +45,43 @@ class ShareReceiverActivity : AppCompatActivity() {
intent.action == Intent.ACTION_SEND -> feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT)
intent.action == Intent.ACTION_VIEW -> feedUrl = intent.dataString
}
-
- if (!feedUrl.isNullOrBlank() && !feedUrl.startsWith("http")) {
+ if (feedUrl.isNullOrBlank()) {
+ Log.e(TAG, "feedUrl is empty or null.")
+ showNoPodcastFoundError()
+ return
+ }
+ if (!feedUrl.startsWith("http")) {
val uri = Uri.parse(feedUrl)
val urlString = uri?.getQueryParameter("url")
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
}
-
+ Logd(TAG, "feedUrl: $feedUrl")
when {
- feedUrl.isNullOrBlank() -> {
- Log.e(TAG, "feedUrl is empty or null.")
- showNoPodcastFoundError()
- }
// plain text
- feedUrl.matches(Regex("^[^\\s<>/]+\$")) -> {
+ feedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> {
val intent = MainActivity.showOnlineSearch(this, feedUrl)
startActivity(intent)
finish()
}
+// Youtube media
+ feedUrl.startsWith("https://youtube.com/watch?") -> {
+ Logd(TAG, "got youtube media")
+ CoroutineScope(Dispatchers.IO).launch {
+ val info = StreamInfo.getInfo(Vista.getService(0), feedUrl)
+ Logd(TAG, "info: $info")
+ val episode = episodeFromStreamInfo(info)
+ Logd(TAG, "episode: $episode")
+ withContext(Dispatchers.Main) {
+ setContent {
+ val showDialog = remember { mutableStateOf(true) }
+ CustomTheme(this@ShareReceiverActivity) {
+ confirmAddEpisode(showDialog.value, episode, onDismissRequest = { showDialog.value = false })
+ }
+ }
+ }
+ }
+
+ }
else -> {
Logd(TAG, "Activity was started with url $feedUrl")
val intent = MainActivity.showOnlineFeed(this, feedUrl)
@@ -52,6 +92,44 @@ class ShareReceiverActivity : AppCompatActivity() {
}
}
+ @Composable
+ fun confirmAddEpisode(showDialog: Boolean, episode: Episode, onDismissRequest: () -> Unit) {
+ if (showDialog) {
+ Dialog(onDismissRequest = { onDismissRequest() }) {
+ Card(
+ modifier = Modifier
+ .wrapContentSize(align = Alignment.Center)
+ .padding(16.dp),
+ shape = RoundedCornerShape(16.dp),
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ var checked by remember { mutableStateOf(false) }
+ Row(Modifier.fillMaxWidth()) {
+ Checkbox(checked = checked,
+ onCheckedChange = {
+ checked = it
+ }
+ )
+ Text(
+ text = stringResource(R.string.pref_video_mode_audio_only),
+ style = MaterialTheme.typography.body1.merge(),
+ )
+ }
+ Button(onClick = {
+ addToYoutubeSyndicate(episode, !checked)
+ finish()
+ }) {
+ Text("Confirm")
+ }
+ }
+ }
+ }
+ }
+ }
+
private fun showNoPodcastFoundError() {
runOnUiThread {
MaterialAlertDialogBuilder(this@ShareReceiverActivity)
@@ -71,8 +149,9 @@ class ShareReceiverActivity : AppCompatActivity() {
}
companion object {
+ private val TAG: String = ShareReceiverActivity::class.simpleName ?: "Anonymous"
+
const val ARG_FEEDURL: String = "arg.feedurl"
private const val RESULT_ERROR = 2
- private val TAG: String = ShareReceiverActivity::class.simpleName ?: "Anonymous"
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
index 5ef9d61b..ea5a4120 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
@@ -270,7 +270,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
if (hasImages) {
holder.image.visibility = View.VISIBLE
if (sc.imageUrl.isNullOrEmpty()) {
-// Glide.with(context).clear(holder.image)
val imageLoader = ImageLoader.Builder(context).build()
imageLoader.enqueue(ImageRequest.Builder(context).data(null).target(holder.image).build())
} else {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
index 13cd6e4f..47240324 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt
@@ -541,7 +541,7 @@ import java.util.concurrent.Semaphore
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
- }
+ } else binding.header.imgvCover.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_launcher_foreground))
}
private var loadItemsRunning = false
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt
index 4f857c30..f81f5601 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt
@@ -100,37 +100,39 @@ class FeedSettingsFragment : Fragment() {
modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 10.dp, bottom = 10.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- // refresh
- Column {
- Row(Modifier.fillMaxWidth()) {
- Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor)
- Spacer(modifier = Modifier.width(20.dp))
+ if ((feed?.id ?: 0) > 10) {
+ // refresh
+ Column {
+ Row(Modifier.fillMaxWidth()) {
+ Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor)
+ Spacer(modifier = Modifier.width(20.dp))
+ Text(
+ text = stringResource(R.string.keep_updated),
+ style = MaterialTheme.typography.h6,
+ color = textColor
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) }
+ Switch(
+ checked = checked,
+ modifier = Modifier.height(24.dp),
+ onCheckedChange = {
+ checked = it
+ feed = upsertBlk(feed!!) { f ->
+ f.preferences?.keepUpdated = checked
+ }
+ }
+ )
+ }
Text(
- text = stringResource(R.string.keep_updated),
- style = MaterialTheme.typography.h6,
+ text = stringResource(R.string.keep_updated_summary),
+ style = MaterialTheme.typography.body2,
color = textColor
)
- Spacer(modifier = Modifier.weight(1f))
- var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) }
- Switch(
- checked = checked,
- modifier = Modifier.height(24.dp),
- onCheckedChange = {
- checked = it
- feed = upsertBlk(feed!!) { f ->
- f.preferences?.keepUpdated = checked
- }
- }
- )
}
- Text(
- text = stringResource(R.string.keep_updated_summary),
- style = MaterialTheme.typography.body2,
- color = textColor
- )
}
- if (feed?.hasVideoMedia == true) {
- // prefer play audio only
+ if ((feed?.id?:0) > 10 && feed?.hasVideoMedia == true) {
+ // video mode
Column {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
@@ -383,7 +385,7 @@ class FeedSettingsFragment : Fragment() {
)
}
// authentication
- if (feed?.isLocalFeed != true) {
+ if ((feed?.id?:0) > 0 && feed?.isLocalFeed != true) {
Column {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
index 8cee4e48..fee20f29 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
@@ -270,7 +270,7 @@ class OnlineFeedFragment : Fragment() {
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
val eList: RealmList = realmListOf()
for (r in channelTabInfo.relatedItems) {
-// Logd(TAG, "startFeedBuilding relatedItem: $r")
+ Logd(TAG, "startFeedBuilding relatedItem: $r")
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
e.feed = feed_
e.feedId = feed_.id
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt
index 09703e37..e3a7bed6 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt
@@ -122,6 +122,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private var useGrid: Boolean? = null
private val useGridLayout: Boolean
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefFeedGridLayout.name, false)
+ private val swipeToRefresh: Boolean
+ get() = appPrefs.getBoolean(UserPreferences.Prefs.prefSwipeToRefreshAll.name, true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -193,10 +195,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
}
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
- binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
- binding.swipeRefresh.setOnRefreshListener {
- FeedUpdateManager.runOnceOrAsk(requireContext())
- }
+
val speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
speedDialView = speedDialBinding.fabSD
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
@@ -216,6 +215,22 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
return binding.root
}
+ private fun setSwipeRefresh() {
+ if (swipeToRefresh) {
+ binding.swipeRefresh.isEnabled = true
+ binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
+ binding.swipeRefresh.setOnRefreshListener {
+ FeedUpdateManager.runOnceOrAsk(requireContext())
+ }
+ } else binding.swipeRefresh.isEnabled = false
+ }
+
+ override fun onResume() {
+ Logd(TAG, "onResume() called")
+ super.onResume()
+ setSwipeRefresh()
+ }
+
private fun initAdapter() {
if (useGrid != useGridLayout) {
useGrid = useGridLayout
@@ -376,11 +391,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} else binding.txtvInformation.visibility = View.GONE
emptyView.updateVisibility()
}
- } catch (e: Throwable) {
- Log.e(TAG, Log.getStackTraceString(e))
- } finally {
- loadItemsRunning = false
- }
+ } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
+ } finally { loadItemsRunning = false }
}
}
}
@@ -973,12 +985,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
count.visibility = View.VISIBLE
val mainActRef = (activity as MainActivity)
- val coverLoader = CoverLoader(mainActRef)
- coverLoader.withUri(feed.imageUrl)
- errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
-
- coverLoader.withCoverView(coverImage)
- coverLoader.load()
+ if (feed.imageUrl != null) {
+ val coverLoader = CoverLoader(mainActRef)
+ coverLoader.withUri(feed.imageUrl)
+ errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
+ coverLoader.withCoverView(coverImage)
+ coverLoader.load()
+ } else coverImage.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_launcher_foreground))
val density: Float = mainActRef.resources.displayMetrics.density
binding.outerContainer.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/CoverLoader.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/CoverLoader.kt
index f921aea8..a848fcb2 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/CoverLoader.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/CoverLoader.kt
@@ -60,9 +60,7 @@ class CoverLoader(private val activity: MainActivity) {
fun load() {
if (imgvCover == null) return
-
val coverTargetCoil = CoilCoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
-
if (resource != 0) {
val imageLoader = ImageLoader.Builder(activity).build()
imageLoader.enqueue(ImageRequest.Builder(activity).data(null).target(coverTargetCoil).build())
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9e49ac64..7609d6cb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -540,6 +540,8 @@
Full screen
Small window
Audio only
+ Swipe to refresh
+ Swipe down to refresh all subscriptions
Subscriptions use grid layout
When set, Subscriptions view use a grid layout, otherwise list layout
High notification priority
diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml
index d0fc8297..2776a6ce 100644
--- a/app/src/main/res/xml/preferences_user_interface.xml
+++ b/app/src/main/res/xml/preferences_user_interface.xml
@@ -38,18 +38,18 @@
android:title="@string/pref_nav_drawer_feed_order_title"
android:key="prefDrawerFeedOrder"
android:summary="@string/pref_nav_drawer_feed_order_sum"/>
+
-
-
-
-
-
-
= 3) saveRated()
else {
resetStartDate()
- mPreferences
- .edit()
- .putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1)
- .apply()
+ mPreferences.edit().putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1).apply()
}
Logd("ReviewDialog", "Successfully finished in-app review")
}
@@ -74,17 +69,11 @@ object RatingDialog {
@VisibleForTesting
fun saveRated() {
- mPreferences
- .edit()
- .putBoolean(KEY_RATED, true)
- .apply()
+ mPreferences.edit().putBoolean(KEY_RATED, true).apply()
}
private fun resetStartDate() {
- mPreferences
- .edit()
- .putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
- .apply()
+ mPreferences.edit().putLong(KEY_FIRST_START_DATE, System.currentTimeMillis()).apply()
}
private fun shouldShow(): Boolean {
diff --git a/app/src/play/kotlin/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt b/app/src/play/kotlin/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt
index 8799e0a8..c395cf2d 100644
--- a/app/src/play/kotlin/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt
+++ b/app/src/play/kotlin/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt
@@ -8,13 +8,10 @@ import com.google.android.gms.security.ProviderInstaller
object SslProviderInstaller {
fun install(context: Context) {
- try {
- ProviderInstaller.installIfNeeded(context)
+ try { ProviderInstaller.installIfNeeded(context)
} catch (e: GooglePlayServicesRepairableException) {
e.printStackTrace()
GoogleApiAvailability.getInstance().showErrorNotification(context, e.connectionStatusCode)
- } catch (e: GooglePlayServicesNotAvailableException) {
- e.printStackTrace()
- }
+ } catch (e: GooglePlayServicesNotAvailableException) { e.printStackTrace() }
}
}
diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt
index 7dae798c..0d82458a 100644
--- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt
+++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt
@@ -20,8 +20,7 @@ abstract class CastEnabledActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
if (canCast) {
- try {
- CastContext.getSharedInstance(this)
+ try { CastContext.getSharedInstance(this)
} catch (e: Exception) {
e.printStackTrace()
canCast = false
@@ -30,9 +29,7 @@ abstract class CastEnabledActivity : AppCompatActivity() {
}
fun requestCastButton(menu: Menu?) {
- if (!canCast) {
- return
- }
+ if (!canCast) return
menuInflater.inflate(R.menu.cast_button, menu)
CastButtonFactory.setUpMediaRouteButton(applicationContext, menu!!, R.id.media_route_menu_item)
}
diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastOptionsProvider.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastOptionsProvider.kt
index 4d881fe3..ab8a5677 100644
--- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastOptionsProvider.kt
+++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastOptionsProvider.kt
@@ -10,9 +10,7 @@ import com.google.android.gms.cast.framework.SessionProvider
@SuppressLint("VisibleForTests")
class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
- return CastOptions.Builder()
- .setReceiverApplicationId("BEBC1DB1")
- .build()
+ return CastOptions.Builder().setReceiverApplicationId("BEBC1DB1").build()
}
override fun getAdditionalSessionProviders(context: Context): List? {
diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastPsmp.kt
index 2772c217..5422a3ea 100644
--- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastPsmp.kt
+++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastPsmp.kt
@@ -29,17 +29,14 @@ import kotlin.math.min
*/
@SuppressLint("VisibleForTests")
class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBase(context, callback) {
-
val TAG = this::class.simpleName ?: "Anonymous"
@Volatile
private var remoteMedia: MediaInfo? = null
-
@Volatile
private var remoteState: Int
private val castContext = CastContext.getSharedInstance(context)
private val remoteMediaClient = castContext.sessionManager.currentCastSession!!.remoteMediaClient
-
private val isBuffering: AtomicBoolean
private val remoteMediaClientCallback: RemoteMediaClient.Callback = object : RemoteMediaClient.Callback() {
@@ -47,17 +44,14 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
super.onMetadataUpdated()
onRemoteMediaPlayerStatusUpdated()
}
-
override fun onPreloadStatusUpdated() {
super.onPreloadStatusUpdated()
onRemoteMediaPlayerStatusUpdated()
}
-
override fun onStatusUpdated() {
super.onStatusUpdated()
onRemoteMediaPlayerStatusUpdated()
}
-
override fun onMediaError(mediaError: MediaError) {
EventFlow.postEvent(FlowEvent.PlayerErrorEvent(mediaError.reason!!))
}
@@ -81,7 +75,6 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
private fun localVersion(info: MediaInfo?): Playable? {
if (info == null || info.metadata == null) return null
if (CastUtils.matches(info, curMedia)) return curMedia
-
val streamUrl = info.metadata!!.getString(CastUtils.KEY_STREAM_URL)
return if (streamUrl == null) CastUtils.makeRemoteMedia(info) else callback.findMedia(streamUrl)
}
@@ -122,17 +115,13 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
state = MediaStatus.PLAYER_STATE_UNKNOWN
stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN
}
-
if (stateChanged) remoteState = state
-
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && state != MediaStatus.PLAYER_STATE_IDLE) {
callback.onPlaybackPause(null, Playable.INVALID_TIME)
// We don't want setPlayerStatus to handle the onPlaybackPause callback
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
}
-
setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING)
-
when (state) {
MediaStatus.PLAYER_STATE_PLAYING -> {
if (!stateChanged) {
@@ -198,10 +187,6 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
}
}
- override fun createMediaPlayer() {}
-
-// private var prevMedia: Playable? = null
-
/**
* Internal implementation of playMediaObject. This method has an additional parameter that
* allows the caller to force a media player reset even if
@@ -275,7 +260,6 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
setPlayerStatus(PlayerStatus.PREPARING, curMedia)
var position = curMedia!!.getPosition()
if (position > 0) position = calculatePositionWithRewind(position, curMedia!!.getLastPlayedTime())
-
remoteMediaClient!!.load(MediaLoadRequestData.Builder()
.setMediaInfo(remoteMedia)
.setAutoplay(startWhenPrepared.get())
@@ -346,14 +330,12 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
var nextMedia: Playable? = null
if (shouldContinue) {
nextMedia = callback.getNextInQueue(currentMedia)
-
val playNextEpisode = isPlaying && nextMedia != null
when {
playNextEpisode -> Logd(TAG, "Playback of next episode will start immediately.")
nextMedia == null -> Logd(TAG, "No more episodes available to play")
else -> Logd(TAG, "Loading next episode, but not playing automatically.")
}
-
if (nextMedia != null) {
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode)
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
@@ -381,12 +363,7 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
fun getInstanceIfConnected(context: Context, callback: MediaPlayerCallback): MediaPlayerBase? {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) return null
-
- try {
- if (CastContext.getSharedInstance(context).castState == CastState.CONNECTED) return CastPsmp(context, callback)
- } catch (e: Exception) {
- e.printStackTrace()
- }
+ try { if (CastContext.getSharedInstance(context).castState == CastState.CONNECTED) return CastPsmp(context, callback) } catch (e: Exception) { e.printStackTrace() }
return null
}
}
diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastStateListener.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastStateListener.kt
index b26764c4..89ae923b 100644
--- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastStateListener.kt
+++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastStateListener.kt
@@ -11,9 +11,8 @@ open class CastStateListener(context: Context) : SessionManagerListener 0) {
- builder.setStreamDuration(media.getDuration().toLong())
- }
+ if (media.getDuration() > 0) builder.setStreamDuration(media.getDuration().toLong())
return builder.build()
}
@@ -66,9 +52,7 @@ object MediaInfoCreator {
* @return [MediaInfo] object in a format proper for casting.
*/
fun from(media: EpisodeMedia?): MediaInfo? {
- if (media == null) {
- return null
- }
+ if (media == null) return null
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC)
checkNotNull(media.episode) { "item is null" }
val feedItem = media.episode
@@ -77,35 +61,21 @@ object MediaInfoCreator {
val subtitle = media.getFeedTitle()
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle)
-
val feed: Feed? = feedItem.feed
// Manual because cast does not support embedded images
val url: String = if (feedItem.imageUrl == null && feed != null) feed.imageUrl?:"" else feedItem.imageUrl?:""
- if (url.isNotEmpty()) {
- metadata.addImage(WebImage(Uri.parse(url)))
- }
+ if (url.isNotEmpty()) metadata.addImage(WebImage(Uri.parse(url)))
val calendar = Calendar.getInstance()
if (media.episode?.getPubDate() != null) calendar.time = media.episode!!.getPubDate()!!
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
if (feed != null) {
- if (!feed.author.isNullOrEmpty()) {
- metadata.putString(MediaMetadata.KEY_ARTIST, feed.author!!)
- }
- if (!feed.downloadUrl.isNullOrEmpty()) {
- metadata.putString(CastUtils.KEY_FEED_URL, feed.downloadUrl!!)
- }
- if (!feed.link.isNullOrEmpty()) {
- metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.link!!)
- }
- }
- if (!feedItem.identifier.isNullOrEmpty()) {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.identifier!!)
- } else {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl()?:"")
- }
- if (!feedItem.link.isNullOrEmpty()) {
- metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.link!!)
+ if (!feed.author.isNullOrEmpty()) metadata.putString(MediaMetadata.KEY_ARTIST, feed.author!!)
+ if (!feed.downloadUrl.isNullOrEmpty()) metadata.putString(CastUtils.KEY_FEED_URL, feed.downloadUrl!!)
+ if (!feed.link.isNullOrEmpty()) metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.link!!)
}
+ if (!feedItem.identifier.isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.identifier!!)
+ else metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl() ?: "")
+ if (!feedItem.link.isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.link!!)
}
// This field only identifies the id on the device that has the original version.
// Idea is to perhaps, on a first approach, check if the version on the local DB with the
@@ -121,9 +91,7 @@ object MediaInfoCreator {
.setContentType(media.mimeType)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(metadata)
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration().toLong())
- }
+ if (media.getDuration() > 0) builder.setStreamDuration(media.getDuration().toLong())
return builder.build()
}
}
diff --git a/changelog.md b/changelog.md
index d7dccbcb..e3da45e5 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,16 @@
+# 6.6.0
+
+* added ability to receive shared Youtube media,
+ * once received, the user can choose to set it as "audio only" before confirm
+ * the media is then added as an episode to one of the two artificial podcasts: "Youtube Syndicate" or "Youtube Syndicate Audio" (for audio-only media)
+ * the two artificial podcasts behave as normal Youtube-channel podcasts except that they can not be updated, and video mode and authentication can not be changed,
+ * the episodes can be handled in the same fashion as normal podcast episodes, except that those in "Youtube Syndicate Audio" can not be played with video
+* fixed info display on notification panel for Youtube episodes
+* added a setting to disable "swipe to refresh all subscriptions" under Settings -> Interface -> Subscriptions
+ * even when disabled, subscriptions can be refreshed from the menu in Subscriptions view
+ * this doesn't affect "swipe to refresh" in FeedEpisodes view for single podcast
+* updated various compose dependencies
+
# 6.5.10
* fixed crash when switching to a newly created queue in Queues view
diff --git a/fastlane/metadata/android/en-US/changelogs/3020245.txt b/fastlane/metadata/android/en-US/changelogs/3020245.txt
new file mode 100644
index 00000000..5082dc77
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3020245.txt
@@ -0,0 +1,12 @@
+ Version 6.5.10:
+
+* added ability to receive shared Youtube media,
+ * once received, the user can choose to set it as "audio only" before confirm
+ * the media is then added as an episode to one of the two artificial podcasts: "Youtube Syndicate" or "Youtube Syndicate Audio" (for audio-only media)
+ * the two artificial podcasts behave as normal Youtube-channel podcasts except that they can not be updated, and video mode and authentication can not be changed,
+ * the episodes can be handled in the same fashion as normal podcast episodes, except that those in "Youtube Syndicate Audio" can not be played with video
+* fixed info display on notification panel for Youtube episodes
+* added a setting to disable "swipe to refresh all subscriptions" under Settings -> Interface -> Subscriptions
+ * even when disabled, subscriptions can be refreshed from the menu in Subscriptions view
+ * this doesn't affect "swipe to refresh" in FeedEpisodes view for single podcast
+* updated various compose dependencies
diff --git a/gradle.properties b/gradle.properties
index 9fea1c09..2e5c95eb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,5 +7,5 @@ android.nonFinalResIds=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
-kotlin.daemon.jvmargs=-Xmx2048m
+#kotlin.daemon.jvmargs=-Xmx1g
org.gradle.configuration-cache=true