6.6.0 commit
This commit is contained in:
parent
ddc0f94d89
commit
1141938b73
13
README.md
13
README.md
|
@ -12,6 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
|||
[<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/)
|
||||
[<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.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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<Int, Int>? {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MediaSource> = ArrayList()
|
||||
mediaSources.add(vSource)
|
||||
mediaSources.add(aSource)
|
||||
|
|
|
@ -286,6 +286,7 @@ object UserPreferences {
|
|||
prefDrawerFeedOrder,
|
||||
prefDrawerFeedOrderDir,
|
||||
prefFeedGridLayout,
|
||||
prefSwipeToRefreshAll,
|
||||
prefExpandNotify,
|
||||
prefEpisodeCover,
|
||||
showTimeLeft,
|
||||
|
|
|
@ -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<Episode>) : 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/*")
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -270,7 +270,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
val eList: RealmList<Episode> = 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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -540,6 +540,8 @@
|
|||
<string name="pref_video_mode_full_screen">Full screen</string>
|
||||
<string name="pref_video_mode_small_window">Small window</string>
|
||||
<string name="pref_video_mode_audio_only">Audio only</string>
|
||||
<string name="pref_swipe_refresh_title">Swipe to refresh</string>
|
||||
<string name="pref_swipe_refresh_sum">Swipe down to refresh all subscriptions</string>
|
||||
<string name="pref_feedGridLayout_title">Subscriptions use grid layout </string>
|
||||
<string name="pref_feedGridLayout_sum">When set, Subscriptions view use a grid layout, otherwise list layout</string>
|
||||
<string name="pref_expandNotify_title">High notification priority</string>
|
||||
|
|
|
@ -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"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefSwipeToRefreshAll"
|
||||
android:summary="@string/pref_swipe_refresh_sum"
|
||||
android:title="@string/pref_swipe_refresh_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefFeedGridLayout"
|
||||
android:summary="@string/pref_feedGridLayout_sum"
|
||||
android:title="@string/pref_feedGridLayout_title"/>
|
||||
|
||||
<!-- <SwitchPreferenceCompat-->
|
||||
<!-- android:title="@string/pref_show_subscription_title"-->
|
||||
<!-- android:key="prefSubscriptionTitle"-->
|
||||
<!-- android:summary="@string/pref_show_subscription_title_summary"-->
|
||||
<!-- android:defaultValue="true" />-->
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/external_elements">
|
||||
<SwitchPreferenceCompat
|
||||
|
|
|
@ -35,9 +35,7 @@ object RatingDialog {
|
|||
}
|
||||
|
||||
fun check() {
|
||||
if (shouldShow()) {
|
||||
try { showInAppReview() } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||
}
|
||||
if (shouldShow()) try { showInAppReview() } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||
}
|
||||
|
||||
private fun showInAppReview() {
|
||||
|
@ -55,10 +53,7 @@ object RatingDialog {
|
|||
if (previousAttempts >= 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 {
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<SessionProvider>? {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,8 @@ open class CastStateListener(context: Context) : SessionManagerListener<CastSess
|
|||
private var castContext: CastContext?
|
||||
|
||||
init {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
||||
castContext = null
|
||||
} else {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) castContext = null
|
||||
else {
|
||||
var castCtx: CastContext?
|
||||
try {
|
||||
castCtx = CastContext.getSharedInstance(context)
|
||||
|
@ -27,40 +26,30 @@ open class CastStateListener(context: Context) : SessionManagerListener<CastSess
|
|||
}
|
||||
|
||||
fun destroy() {
|
||||
if (castContext != null) {
|
||||
castContext!!.sessionManager.removeSessionManagerListener(this, CastSession::class.java)
|
||||
}
|
||||
if (castContext != null) castContext!!.sessionManager.removeSessionManagerListener(this, CastSession::class.java)
|
||||
}
|
||||
|
||||
override fun onSessionStarting(castSession: CastSession) {
|
||||
}
|
||||
override fun onSessionStarting(castSession: CastSession) {}
|
||||
|
||||
override fun onSessionStarted(session: CastSession, sessionId: String) {
|
||||
onSessionStartedOrEnded()
|
||||
}
|
||||
|
||||
override fun onSessionStartFailed(castSession: CastSession, i: Int) {
|
||||
}
|
||||
override fun onSessionStartFailed(castSession: CastSession, i: Int) {}
|
||||
|
||||
override fun onSessionEnding(castSession: CastSession) {
|
||||
}
|
||||
override fun onSessionEnding(castSession: CastSession) {}
|
||||
|
||||
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
|
||||
}
|
||||
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {}
|
||||
|
||||
override fun onSessionResumeFailed(castSession: CastSession, i: Int) {
|
||||
}
|
||||
override fun onSessionResumeFailed(castSession: CastSession, i: Int) {}
|
||||
|
||||
override fun onSessionSuspended(castSession: CastSession, i: Int) {
|
||||
}
|
||||
override fun onSessionSuspended(castSession: CastSession, i: Int) {}
|
||||
|
||||
override fun onSessionEnded(session: CastSession, error: Int) {
|
||||
onSessionStartedOrEnded()
|
||||
}
|
||||
|
||||
override fun onSessionResuming(castSession: CastSession, s: String) {
|
||||
}
|
||||
override fun onSessionResuming(castSession: CastSession, s: String) {}
|
||||
|
||||
open fun onSessionStartedOrEnded() {
|
||||
}
|
||||
open fun onSessionStartedOrEnded() {}
|
||||
}
|
||||
|
|
|
@ -121,8 +121,8 @@ object CastUtils {
|
|||
if (info.contentId != media.getStreamUrl()) return false
|
||||
|
||||
val metadata = info.metadata
|
||||
return (metadata != null && metadata.getString(KEY_EPISODE_IDENTIFIER) ==
|
||||
media.getEpisodeIdentifier() && metadata.getString(KEY_FEED_URL) == media.feedUrl)
|
||||
return (metadata != null && metadata.getString(KEY_EPISODE_IDENTIFIER) == media.getEpisodeIdentifier()
|
||||
&& metadata.getString(KEY_FEED_URL) == media.feedUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,33 +15,21 @@ object MediaInfoCreator {
|
|||
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle())
|
||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle())
|
||||
if (!media.getImageLocation().isNullOrEmpty()) {
|
||||
metadata.addImage(WebImage(Uri.parse(media.getImageLocation())))
|
||||
}
|
||||
if (!media.getImageLocation().isNullOrEmpty()) metadata.addImage(WebImage(Uri.parse(media.getImageLocation())))
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.time = media.getPubDate()
|
||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
||||
if (media.getFeedAuthor().isNotEmpty()) {
|
||||
metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor())
|
||||
}
|
||||
if (!media.feedUrl.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_FEED_URL, media.feedUrl)
|
||||
}
|
||||
if (!media.feedLink.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.feedLink)
|
||||
}
|
||||
if (!media.getEpisodeIdentifier().isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier()!!)
|
||||
} else {
|
||||
if (media.getFeedAuthor().isNotEmpty()) metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor())
|
||||
if (!media.feedUrl.isNullOrEmpty()) metadata.putString(CastUtils.KEY_FEED_URL, media.feedUrl)
|
||||
if (!media.feedLink.isNullOrEmpty()) metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.feedLink)
|
||||
if (!media.getEpisodeIdentifier().isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier()!!)
|
||||
else {
|
||||
if (media.getStreamUrl() != null) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl()!!)
|
||||
}
|
||||
if (!media.episodeLink.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.episodeLink)
|
||||
}
|
||||
if (!media.episodeLink.isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_LINK, media.episodeLink)
|
||||
|
||||
val notes: String? = media.getDescription()
|
||||
if (notes != null) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes)
|
||||
}
|
||||
if (notes != null) metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes)
|
||||
// Default id value
|
||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0)
|
||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE)
|
||||
|
@ -51,9 +39,7 @@ object MediaInfoCreator {
|
|||
.setContentType(media.getMimeType())
|
||||
.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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
13
changelog.md
13
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue