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 [F-Droid](https://f-droid.org/packages/ac.mdiq.podcini.R/) [Amazon](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