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/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)
|
[<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
|
#### 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.
|
#### 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.
|
#### 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,
|
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
|
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
|
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,
|
9. Offers Readability and Text-to-Speech for RSS contents,
|
||||||
10. Features `instant sync` across devices without a server.
|
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
|
* 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 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
|
* 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
|
### Instant (or Wifi) sync
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020244
|
versionCode 3020245
|
||||||
versionName "6.5.10"
|
versionName "6.6.0"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
@ -172,17 +172,14 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring for using VistaGuide **/
|
/** Desugaring for using VistaGuide **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
|
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 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
|
||||||
|
|
||||||
implementation 'androidx.compose.material:material:1.7.0'
|
def composeBom = platform('androidx.compose:compose-bom:2024.09.01')
|
||||||
|
implementation composeBom
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
|
androidTestImplementation composeBom
|
||||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
|
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.activity:activity-compose:1.9.2'
|
||||||
implementation 'androidx.window:window:1.3.0'
|
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?) {
|
protected open fun setPlayable(playable: Playable?) {
|
||||||
if (playable != null && playable !== curMedia) {
|
if (playable != null && playable !== curMedia) curMedia = playable
|
||||||
curMedia = playable
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getVideoSize(): Pair<Int, Int>? {
|
open fun getVideoSize(): Pair<Int, Int>? {
|
||||||
|
@ -92,7 +90,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
return -1
|
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
|
* 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) {
|
fun seekDelta(delta: Int) {
|
||||||
val curPosition = getPosition()
|
val curPosition = getPosition()
|
||||||
if (curPosition != Playable.INVALID_TIME) {
|
if (curPosition != Playable.INVALID_TIME) seekTo(curPosition + delta)
|
||||||
val prevMedia = curMedia
|
|
||||||
seekTo(curPosition + delta)
|
|
||||||
}
|
|
||||||
else Log.e(TAG, "seekDelta getPosition() returned INVALID_TIME in seekDelta")
|
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
|
@JvmField
|
||||||
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
|
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
|
||||||
|
|
||||||
val audioPlaybackSpeed: Float
|
val prefPlaybackSpeed: Float
|
||||||
get() {
|
get() {
|
||||||
try { return appPrefs.getString(Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
try { return appPrefs.getString(Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
|
@ -374,7 +369,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
|
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = audioPlaybackSpeed
|
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = prefPlaybackSpeed
|
||||||
return playbackSpeed
|
return playbackSpeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -804,7 +804,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
|
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
|
||||||
}
|
}
|
||||||
val playable = curMedia
|
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()}")
|
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) {
|
if (keycode == -1 && playable == null && customAction == null) {
|
||||||
Log.e(TAG, "onStartCommand PlaybackService was started with no arguments, return")
|
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")
|
Logd(TAG, "onStartCommand playing same media: $status, return")
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
keycode != -1 -> {
|
keycode != -1 -> {
|
||||||
Logd(TAG, "onStartCommand Received hardware button event: $hardwareButton")
|
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 audioIndex = if (isNetworkRestricted) 0 else audioStreamsList.size - 1
|
||||||
val audioStream = audioStreamsList[audioIndex]
|
val audioStream = audioStreamsList[audioIndex]
|
||||||
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
|
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
|
||||||
val aSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(
|
val aSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||||
Uri.parse(audioStream.content)).build())
|
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
|
||||||
if (media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
if (media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||||
Logd(TAG, "setDataSource1 result: $streamInfo")
|
Logd(TAG, "setDataSource1 result: $streamInfo")
|
||||||
Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}")
|
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 videoIndex = 0
|
||||||
val videoStream = videoStreamsList[videoIndex]
|
val videoStream = videoStreamsList[videoIndex]
|
||||||
Logd(TAG, "setDataSource1 use video quality: ${videoStream.resolution}")
|
Logd(TAG, "setDataSource1 use video quality: ${videoStream.resolution}")
|
||||||
val vSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(
|
val vSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||||
Uri.parse(videoStream.content)).build())
|
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(videoStream.content)).build())
|
||||||
val mediaSources: MutableList<MediaSource> = ArrayList()
|
val mediaSources: MutableList<MediaSource> = ArrayList()
|
||||||
mediaSources.add(vSource)
|
mediaSources.add(vSource)
|
||||||
mediaSources.add(aSource)
|
mediaSources.add(aSource)
|
||||||
|
|
|
@ -286,6 +286,7 @@ object UserPreferences {
|
||||||
prefDrawerFeedOrder,
|
prefDrawerFeedOrder,
|
||||||
prefDrawerFeedOrderDir,
|
prefDrawerFeedOrderDir,
|
||||||
prefFeedGridLayout,
|
prefFeedGridLayout,
|
||||||
|
prefSwipeToRefreshAll,
|
||||||
prefExpandNotify,
|
prefExpandNotify,
|
||||||
prefEpisodeCover,
|
prefEpisodeCover,
|
||||||
showTimeLeft,
|
showTimeLeft,
|
||||||
|
|
|
@ -29,6 +29,7 @@ import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||||
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -197,39 +198,24 @@ object Episodes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (removedFromQueue.isNotEmpty()) removeFromAllQueuesSync(*removedFromQueue.toTypedArray())
|
if (removedFromQueue.isNotEmpty()) removeFromAllQueuesSync(*removedFromQueue.toTypedArray())
|
||||||
|
|
||||||
for (episode in removedFromQueue) EventFlow.postEvent(FlowEvent.QueueEvent.irreversibleRemoved(episode))
|
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.
|
// 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
|
// especially important if download or refresh failed, as the user should not be able
|
||||||
// to retry these
|
// to retry these
|
||||||
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
|
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
|
||||||
|
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = BackupManager(context)
|
||||||
backupManager.dataChanged()
|
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
|
// only used in tests
|
||||||
fun persistEpisodeMedia(media: EpisodeMedia) : Job {
|
fun persistEpisodeMedia(media: EpisodeMedia) : Job {
|
||||||
Logd(TAG, "persistEpisodeMedia called")
|
Logd(TAG, "persistEpisodeMedia called")
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
var episode = media.episodeOrFetch()
|
var episode = media.episodeOrFetch()
|
||||||
if (episode != null) {
|
if (episode != null) {
|
||||||
episode = upsert(episode) {
|
episode = upsert(episode) { it.media = media }
|
||||||
it.media = media
|
|
||||||
}
|
|
||||||
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode))
|
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode))
|
||||||
} else Log.e(TAG, "persistEpisodeMedia media.episode is null")
|
} else Log.e(TAG, "persistEpisodeMedia media.episode is null")
|
||||||
}
|
}
|
||||||
|
@ -255,9 +241,7 @@ object Episodes {
|
||||||
fun addToHistory(episode: Episode, date: Date? = Date()) : Job {
|
fun addToHistory(episode: Episode, date: Date? = Date()) : Job {
|
||||||
Logd(TAG, "addToHistory called")
|
Logd(TAG, "addToHistory called")
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
upsert(episode) {
|
upsert(episode) { it.media?.playbackCompletionDate = date }
|
||||||
it.media?.playbackCompletionDate = date
|
|
||||||
}
|
|
||||||
EventFlow.postEvent(FlowEvent.HistoryEvent())
|
EventFlow.postEvent(FlowEvent.HistoryEvent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,9 +250,7 @@ object Episodes {
|
||||||
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
|
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
|
||||||
Logd(TAG, "setFavorite called $stat")
|
Logd(TAG, "setFavorite called $stat")
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
val result = upsert(episode) {
|
val result = upsert(episode) { it.isFavorite = stat ?: !it.isFavorite }
|
||||||
it.isFavorite = stat ?: !it.isFavorite
|
|
||||||
}
|
|
||||||
EventFlow.postEvent(FlowEvent.FavoritesEvent(result))
|
EventFlow.postEvent(FlowEvent.FavoritesEvent(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,11 +291,25 @@ object Episodes {
|
||||||
return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
|
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()
|
val e = Episode()
|
||||||
e.link = info.url
|
e.link = info.url
|
||||||
e.title = info.name
|
e.title = info.name
|
||||||
e.description = info.shortDescription
|
e.description = info.description?.content
|
||||||
e.imageUrl = info.thumbnails.first().url
|
e.imageUrl = info.thumbnails.first().url
|
||||||
e.setPubDate(info.uploadDate?.date()?.time)
|
e.setPubDate(info.uploadDate?.date()?.time)
|
||||||
val m = EpisodeMedia(e, info.url, 0, "video/*")
|
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.model.EpisodeAction
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
|
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.isAutoDelete
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
|
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
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.LogsAndStats.addDownloadStatus
|
||||||
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
|
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
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.AutoDeleteAction
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
|
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
|
||||||
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting
|
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.Logd
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
@ -412,6 +416,43 @@ object Feeds {
|
||||||
return !feed.isLocalFeed || isAutoDeleteLocal
|
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
|
* 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.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
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.storage.utils.MediaMetadataRetrieverCompat
|
||||||
import ac.mdiq.podcini.util.Logd
|
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.content.Context
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
|
@ -1,20 +1,41 @@
|
||||||
package ac.mdiq.podcini.ui.activity
|
package ac.mdiq.podcini.ui.activity
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate
|
||||||
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.util.Logd
|
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.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.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 androidx.media3.common.util.UnstableApi
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class ShareReceiverActivity : AppCompatActivity() {
|
class ShareReceiverActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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_SEND -> feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
intent.action == Intent.ACTION_VIEW -> feedUrl = intent.dataString
|
intent.action == Intent.ACTION_VIEW -> feedUrl = intent.dataString
|
||||||
}
|
}
|
||||||
|
if (feedUrl.isNullOrBlank()) {
|
||||||
if (!feedUrl.isNullOrBlank() && !feedUrl.startsWith("http")) {
|
Log.e(TAG, "feedUrl is empty or null.")
|
||||||
|
showNoPodcastFoundError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!feedUrl.startsWith("http")) {
|
||||||
val uri = Uri.parse(feedUrl)
|
val uri = Uri.parse(feedUrl)
|
||||||
val urlString = uri?.getQueryParameter("url")
|
val urlString = uri?.getQueryParameter("url")
|
||||||
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
|
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
|
||||||
}
|
}
|
||||||
|
Logd(TAG, "feedUrl: $feedUrl")
|
||||||
when {
|
when {
|
||||||
feedUrl.isNullOrBlank() -> {
|
|
||||||
Log.e(TAG, "feedUrl is empty or null.")
|
|
||||||
showNoPodcastFoundError()
|
|
||||||
}
|
|
||||||
// plain text
|
// plain text
|
||||||
feedUrl.matches(Regex("^[^\\s<>/]+\$")) -> {
|
feedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> {
|
||||||
val intent = MainActivity.showOnlineSearch(this, feedUrl)
|
val intent = MainActivity.showOnlineSearch(this, feedUrl)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
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 -> {
|
else -> {
|
||||||
Logd(TAG, "Activity was started with url $feedUrl")
|
Logd(TAG, "Activity was started with url $feedUrl")
|
||||||
val intent = MainActivity.showOnlineFeed(this, 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() {
|
private fun showNoPodcastFoundError() {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
MaterialAlertDialogBuilder(this@ShareReceiverActivity)
|
MaterialAlertDialogBuilder(this@ShareReceiverActivity)
|
||||||
|
@ -71,8 +149,9 @@ class ShareReceiverActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val TAG: String = ShareReceiverActivity::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||||
private const val RESULT_ERROR = 2
|
private const val RESULT_ERROR = 2
|
||||||
private val TAG: String = ShareReceiverActivity::class.simpleName ?: "Anonymous"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,7 +270,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
holder.image.visibility = View.VISIBLE
|
holder.image.visibility = View.VISIBLE
|
||||||
if (sc.imageUrl.isNullOrEmpty()) {
|
if (sc.imageUrl.isNullOrEmpty()) {
|
||||||
// Glide.with(context).clear(holder.image)
|
|
||||||
val imageLoader = ImageLoader.Builder(context).build()
|
val imageLoader = ImageLoader.Builder(context).build()
|
||||||
imageLoader.enqueue(ImageRequest.Builder(context).data(null).target(holder.image).build())
|
imageLoader.enqueue(ImageRequest.Builder(context).data(null).target(holder.image).build())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -541,7 +541,7 @@ import java.util.concurrent.Semaphore
|
||||||
placeholder(R.color.light_gray)
|
placeholder(R.color.light_gray)
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
}
|
} else binding.header.imgvCover.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_launcher_foreground))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadItemsRunning = false
|
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),
|
modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 10.dp, bottom = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// refresh
|
if ((feed?.id ?: 0) > 10) {
|
||||||
Column {
|
// refresh
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Column {
|
||||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "", tint = textColor)
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Spacer(modifier = Modifier.width(20.dp))
|
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(
|
||||||
text = stringResource(R.string.keep_updated),
|
text = stringResource(R.string.keep_updated_summary),
|
||||||
style = MaterialTheme.typography.h6,
|
style = MaterialTheme.typography.body2,
|
||||||
color = textColor
|
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) {
|
if ((feed?.id?:0) > 10 && feed?.hasVideoMedia == true) {
|
||||||
// prefer play audio only
|
// video mode
|
||||||
Column {
|
Column {
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
|
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
|
||||||
|
@ -383,7 +385,7 @@ class FeedSettingsFragment : Fragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// authentication
|
// authentication
|
||||||
if (feed?.isLocalFeed != true) {
|
if ((feed?.id?:0) > 0 && feed?.isLocalFeed != true) {
|
||||||
Column {
|
Column {
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor)
|
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
|
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||||
val eList: RealmList<Episode> = realmListOf()
|
val eList: RealmList<Episode> = realmListOf()
|
||||||
for (r in channelTabInfo.relatedItems) {
|
for (r in channelTabInfo.relatedItems) {
|
||||||
// Logd(TAG, "startFeedBuilding relatedItem: $r")
|
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||||
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
|
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
|
||||||
e.feed = feed_
|
e.feed = feed_
|
||||||
e.feedId = feed_.id
|
e.feedId = feed_.id
|
||||||
|
|
|
@ -122,6 +122,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
private var useGrid: Boolean? = null
|
private var useGrid: Boolean? = null
|
||||||
private val useGridLayout: Boolean
|
private val useGridLayout: Boolean
|
||||||
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefFeedGridLayout.name, false)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -193,10 +195,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
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)
|
val speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||||
speedDialView = speedDialBinding.fabSD
|
speedDialView = speedDialBinding.fabSD
|
||||||
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
|
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
|
||||||
|
@ -216,6 +215,22 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
return binding.root
|
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() {
|
private fun initAdapter() {
|
||||||
if (useGrid != useGridLayout) {
|
if (useGrid != useGridLayout) {
|
||||||
useGrid = useGridLayout
|
useGrid = useGridLayout
|
||||||
|
@ -376,11 +391,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
} else binding.txtvInformation.visibility = View.GONE
|
} else binding.txtvInformation.visibility = View.GONE
|
||||||
emptyView.updateVisibility()
|
emptyView.updateVisibility()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
} finally { loadItemsRunning = false }
|
||||||
} finally {
|
|
||||||
loadItemsRunning = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -973,12 +985,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
count.visibility = View.VISIBLE
|
count.visibility = View.VISIBLE
|
||||||
|
|
||||||
val mainActRef = (activity as MainActivity)
|
val mainActRef = (activity as MainActivity)
|
||||||
val coverLoader = CoverLoader(mainActRef)
|
if (feed.imageUrl != null) {
|
||||||
coverLoader.withUri(feed.imageUrl)
|
val coverLoader = CoverLoader(mainActRef)
|
||||||
errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
|
coverLoader.withUri(feed.imageUrl)
|
||||||
|
errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
|
||||||
coverLoader.withCoverView(coverImage)
|
coverLoader.withCoverView(coverImage)
|
||||||
coverLoader.load()
|
coverLoader.load()
|
||||||
|
} else coverImage.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_launcher_foreground))
|
||||||
|
|
||||||
val density: Float = mainActRef.resources.displayMetrics.density
|
val density: Float = mainActRef.resources.displayMetrics.density
|
||||||
binding.outerContainer.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
|
binding.outerContainer.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
|
||||||
|
|
|
@ -60,9 +60,7 @@ class CoverLoader(private val activity: MainActivity) {
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
if (imgvCover == null) return
|
if (imgvCover == null) return
|
||||||
|
|
||||||
val coverTargetCoil = CoilCoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
|
val coverTargetCoil = CoilCoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
|
||||||
|
|
||||||
if (resource != 0) {
|
if (resource != 0) {
|
||||||
val imageLoader = ImageLoader.Builder(activity).build()
|
val imageLoader = ImageLoader.Builder(activity).build()
|
||||||
imageLoader.enqueue(ImageRequest.Builder(activity).data(null).target(coverTargetCoil).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_full_screen">Full screen</string>
|
||||||
<string name="pref_video_mode_small_window">Small window</string>
|
<string name="pref_video_mode_small_window">Small window</string>
|
||||||
<string name="pref_video_mode_audio_only">Audio only</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_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_feedGridLayout_sum">When set, Subscriptions view use a grid layout, otherwise list layout</string>
|
||||||
<string name="pref_expandNotify_title">High notification priority</string>
|
<string name="pref_expandNotify_title">High notification priority</string>
|
||||||
|
|
|
@ -38,18 +38,18 @@
|
||||||
android:title="@string/pref_nav_drawer_feed_order_title"
|
android:title="@string/pref_nav_drawer_feed_order_title"
|
||||||
android:key="prefDrawerFeedOrder"
|
android:key="prefDrawerFeedOrder"
|
||||||
android:summary="@string/pref_nav_drawer_feed_order_sum"/>
|
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
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:key="prefFeedGridLayout"
|
android:key="prefFeedGridLayout"
|
||||||
android:summary="@string/pref_feedGridLayout_sum"
|
android:summary="@string/pref_feedGridLayout_sum"
|
||||||
android:title="@string/pref_feedGridLayout_title"/>
|
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>
|
||||||
<PreferenceCategory android:title="@string/external_elements">
|
<PreferenceCategory android:title="@string/external_elements">
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
|
|
|
@ -35,9 +35,7 @@ object RatingDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun check() {
|
fun check() {
|
||||||
if (shouldShow()) {
|
if (shouldShow()) try { showInAppReview() } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||||
try { showInAppReview() } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showInAppReview() {
|
private fun showInAppReview() {
|
||||||
|
@ -55,10 +53,7 @@ object RatingDialog {
|
||||||
if (previousAttempts >= 3) saveRated()
|
if (previousAttempts >= 3) saveRated()
|
||||||
else {
|
else {
|
||||||
resetStartDate()
|
resetStartDate()
|
||||||
mPreferences
|
mPreferences.edit().putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1).apply()
|
||||||
.edit()
|
|
||||||
.putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1)
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
Logd("ReviewDialog", "Successfully finished in-app review")
|
Logd("ReviewDialog", "Successfully finished in-app review")
|
||||||
}
|
}
|
||||||
|
@ -74,17 +69,11 @@ object RatingDialog {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun saveRated() {
|
fun saveRated() {
|
||||||
mPreferences
|
mPreferences.edit().putBoolean(KEY_RATED, true).apply()
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_RATED, true)
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetStartDate() {
|
private fun resetStartDate() {
|
||||||
mPreferences
|
mPreferences.edit().putLong(KEY_FIRST_START_DATE, System.currentTimeMillis()).apply()
|
||||||
.edit()
|
|
||||||
.putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldShow(): Boolean {
|
private fun shouldShow(): Boolean {
|
||||||
|
|
|
@ -8,13 +8,10 @@ import com.google.android.gms.security.ProviderInstaller
|
||||||
|
|
||||||
object SslProviderInstaller {
|
object SslProviderInstaller {
|
||||||
fun install(context: Context) {
|
fun install(context: Context) {
|
||||||
try {
|
try { ProviderInstaller.installIfNeeded(context)
|
||||||
ProviderInstaller.installIfNeeded(context)
|
|
||||||
} catch (e: GooglePlayServicesRepairableException) {
|
} catch (e: GooglePlayServicesRepairableException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
GoogleApiAvailability.getInstance().showErrorNotification(context, e.connectionStatusCode)
|
GoogleApiAvailability.getInstance().showErrorNotification(context, e.connectionStatusCode)
|
||||||
} catch (e: GooglePlayServicesNotAvailableException) {
|
} catch (e: GooglePlayServicesNotAvailableException) { e.printStackTrace() }
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,7 @@ abstract class CastEnabledActivity : AppCompatActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||||
if (canCast) {
|
if (canCast) {
|
||||||
try {
|
try { CastContext.getSharedInstance(this)
|
||||||
CastContext.getSharedInstance(this)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
canCast = false
|
canCast = false
|
||||||
|
@ -30,9 +29,7 @@ abstract class CastEnabledActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestCastButton(menu: Menu?) {
|
fun requestCastButton(menu: Menu?) {
|
||||||
if (!canCast) {
|
if (!canCast) return
|
||||||
return
|
|
||||||
}
|
|
||||||
menuInflater.inflate(R.menu.cast_button, menu)
|
menuInflater.inflate(R.menu.cast_button, menu)
|
||||||
CastButtonFactory.setUpMediaRouteButton(applicationContext, menu!!, R.id.media_route_menu_item)
|
CastButtonFactory.setUpMediaRouteButton(applicationContext, menu!!, R.id.media_route_menu_item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,7 @@ import com.google.android.gms.cast.framework.SessionProvider
|
||||||
@SuppressLint("VisibleForTests")
|
@SuppressLint("VisibleForTests")
|
||||||
class CastOptionsProvider : OptionsProvider {
|
class CastOptionsProvider : OptionsProvider {
|
||||||
override fun getCastOptions(context: Context): CastOptions {
|
override fun getCastOptions(context: Context): CastOptions {
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder().setReceiverApplicationId("BEBC1DB1").build()
|
||||||
.setReceiverApplicationId("BEBC1DB1")
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
|
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
|
||||||
|
|
|
@ -29,17 +29,14 @@ import kotlin.math.min
|
||||||
*/
|
*/
|
||||||
@SuppressLint("VisibleForTests")
|
@SuppressLint("VisibleForTests")
|
||||||
class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBase(context, callback) {
|
class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBase(context, callback) {
|
||||||
|
|
||||||
val TAG = this::class.simpleName ?: "Anonymous"
|
val TAG = this::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var remoteMedia: MediaInfo? = null
|
private var remoteMedia: MediaInfo? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var remoteState: Int
|
private var remoteState: Int
|
||||||
private val castContext = CastContext.getSharedInstance(context)
|
private val castContext = CastContext.getSharedInstance(context)
|
||||||
private val remoteMediaClient = castContext.sessionManager.currentCastSession!!.remoteMediaClient
|
private val remoteMediaClient = castContext.sessionManager.currentCastSession!!.remoteMediaClient
|
||||||
|
|
||||||
private val isBuffering: AtomicBoolean
|
private val isBuffering: AtomicBoolean
|
||||||
|
|
||||||
private val remoteMediaClientCallback: RemoteMediaClient.Callback = object : RemoteMediaClient.Callback() {
|
private val remoteMediaClientCallback: RemoteMediaClient.Callback = object : RemoteMediaClient.Callback() {
|
||||||
|
@ -47,17 +44,14 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
|
||||||
super.onMetadataUpdated()
|
super.onMetadataUpdated()
|
||||||
onRemoteMediaPlayerStatusUpdated()
|
onRemoteMediaPlayerStatusUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreloadStatusUpdated() {
|
override fun onPreloadStatusUpdated() {
|
||||||
super.onPreloadStatusUpdated()
|
super.onPreloadStatusUpdated()
|
||||||
onRemoteMediaPlayerStatusUpdated()
|
onRemoteMediaPlayerStatusUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStatusUpdated() {
|
override fun onStatusUpdated() {
|
||||||
super.onStatusUpdated()
|
super.onStatusUpdated()
|
||||||
onRemoteMediaPlayerStatusUpdated()
|
onRemoteMediaPlayerStatusUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaError(mediaError: MediaError) {
|
override fun onMediaError(mediaError: MediaError) {
|
||||||
EventFlow.postEvent(FlowEvent.PlayerErrorEvent(mediaError.reason!!))
|
EventFlow.postEvent(FlowEvent.PlayerErrorEvent(mediaError.reason!!))
|
||||||
}
|
}
|
||||||
|
@ -81,7 +75,6 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
|
||||||
private fun localVersion(info: MediaInfo?): Playable? {
|
private fun localVersion(info: MediaInfo?): Playable? {
|
||||||
if (info == null || info.metadata == null) return null
|
if (info == null || info.metadata == null) return null
|
||||||
if (CastUtils.matches(info, curMedia)) return curMedia
|
if (CastUtils.matches(info, curMedia)) return curMedia
|
||||||
|
|
||||||
val streamUrl = info.metadata!!.getString(CastUtils.KEY_STREAM_URL)
|
val streamUrl = info.metadata!!.getString(CastUtils.KEY_STREAM_URL)
|
||||||
return if (streamUrl == null) CastUtils.makeRemoteMedia(info) else callback.findMedia(streamUrl)
|
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
|
state = MediaStatus.PLAYER_STATE_UNKNOWN
|
||||||
stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN
|
stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateChanged) remoteState = state
|
if (stateChanged) remoteState = state
|
||||||
|
|
||||||
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && state != MediaStatus.PLAYER_STATE_IDLE) {
|
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && state != MediaStatus.PLAYER_STATE_IDLE) {
|
||||||
callback.onPlaybackPause(null, Playable.INVALID_TIME)
|
callback.onPlaybackPause(null, Playable.INVALID_TIME)
|
||||||
// We don't want setPlayerStatus to handle the onPlaybackPause callback
|
// We don't want setPlayerStatus to handle the onPlaybackPause callback
|
||||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
|
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING)
|
setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING)
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
MediaStatus.PLAYER_STATE_PLAYING -> {
|
MediaStatus.PLAYER_STATE_PLAYING -> {
|
||||||
if (!stateChanged) {
|
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
|
* Internal implementation of playMediaObject. This method has an additional parameter that
|
||||||
* allows the caller to force a media player reset even if
|
* 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)
|
setPlayerStatus(PlayerStatus.PREPARING, curMedia)
|
||||||
var position = curMedia!!.getPosition()
|
var position = curMedia!!.getPosition()
|
||||||
if (position > 0) position = calculatePositionWithRewind(position, curMedia!!.getLastPlayedTime())
|
if (position > 0) position = calculatePositionWithRewind(position, curMedia!!.getLastPlayedTime())
|
||||||
|
|
||||||
remoteMediaClient!!.load(MediaLoadRequestData.Builder()
|
remoteMediaClient!!.load(MediaLoadRequestData.Builder()
|
||||||
.setMediaInfo(remoteMedia)
|
.setMediaInfo(remoteMedia)
|
||||||
.setAutoplay(startWhenPrepared.get())
|
.setAutoplay(startWhenPrepared.get())
|
||||||
|
@ -346,14 +330,12 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas
|
||||||
var nextMedia: Playable? = null
|
var nextMedia: Playable? = null
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
nextMedia = callback.getNextInQueue(currentMedia)
|
nextMedia = callback.getNextInQueue(currentMedia)
|
||||||
|
|
||||||
val playNextEpisode = isPlaying && nextMedia != null
|
val playNextEpisode = isPlaying && nextMedia != null
|
||||||
when {
|
when {
|
||||||
playNextEpisode -> Logd(TAG, "Playback of next episode will start immediately.")
|
playNextEpisode -> Logd(TAG, "Playback of next episode will start immediately.")
|
||||||
nextMedia == null -> Logd(TAG, "No more episodes available to play")
|
nextMedia == null -> Logd(TAG, "No more episodes available to play")
|
||||||
else -> Logd(TAG, "Loading next episode, but not playing automatically.")
|
else -> Logd(TAG, "Loading next episode, but not playing automatically.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMedia != null) {
|
if (nextMedia != null) {
|
||||||
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode)
|
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode)
|
||||||
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
|
// 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? {
|
fun getInstanceIfConnected(context: Context, callback: MediaPlayerCallback): MediaPlayerBase? {
|
||||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) return null
|
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
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,8 @@ open class CastStateListener(context: Context) : SessionManagerListener<CastSess
|
||||||
private var castContext: CastContext?
|
private var castContext: CastContext?
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) castContext = null
|
||||||
castContext = null
|
else {
|
||||||
} else {
|
|
||||||
var castCtx: CastContext?
|
var castCtx: CastContext?
|
||||||
try {
|
try {
|
||||||
castCtx = CastContext.getSharedInstance(context)
|
castCtx = CastContext.getSharedInstance(context)
|
||||||
|
@ -27,40 +26,30 @@ open class CastStateListener(context: Context) : SessionManagerListener<CastSess
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
if (castContext != null) {
|
if (castContext != null) castContext!!.sessionManager.removeSessionManagerListener(this, CastSession::class.java)
|
||||||
castContext!!.sessionManager.removeSessionManagerListener(this, CastSession::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionStarting(castSession: CastSession) {
|
override fun onSessionStarting(castSession: CastSession) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionStarted(session: CastSession, sessionId: String) {
|
override fun onSessionStarted(session: CastSession, sessionId: String) {
|
||||||
onSessionStartedOrEnded()
|
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) {
|
override fun onSessionEnded(session: CastSession, error: Int) {
|
||||||
onSessionStartedOrEnded()
|
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
|
if (info.contentId != media.getStreamUrl()) return false
|
||||||
|
|
||||||
val metadata = info.metadata
|
val metadata = info.metadata
|
||||||
return (metadata != null && metadata.getString(KEY_EPISODE_IDENTIFIER) ==
|
return (metadata != null && metadata.getString(KEY_EPISODE_IDENTIFIER) == media.getEpisodeIdentifier()
|
||||||
media.getEpisodeIdentifier() && metadata.getString(KEY_FEED_URL) == media.feedUrl)
|
&& metadata.getString(KEY_FEED_URL) == media.feedUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,33 +15,21 @@ object MediaInfoCreator {
|
||||||
|
|
||||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle())
|
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle())
|
||||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle())
|
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle())
|
||||||
if (!media.getImageLocation().isNullOrEmpty()) {
|
if (!media.getImageLocation().isNullOrEmpty()) metadata.addImage(WebImage(Uri.parse(media.getImageLocation())))
|
||||||
metadata.addImage(WebImage(Uri.parse(media.getImageLocation())))
|
|
||||||
}
|
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
calendar.time = media.getPubDate()
|
calendar.time = media.getPubDate()
|
||||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
||||||
if (media.getFeedAuthor().isNotEmpty()) {
|
if (media.getFeedAuthor().isNotEmpty()) metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor())
|
||||||
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.feedUrl.isNullOrEmpty()) {
|
if (!media.getEpisodeIdentifier().isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier()!!)
|
||||||
metadata.putString(CastUtils.KEY_FEED_URL, media.feedUrl)
|
else {
|
||||||
}
|
|
||||||
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.getStreamUrl() != null) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl()!!)
|
||||||
}
|
}
|
||||||
if (!media.episodeLink.isNullOrEmpty()) {
|
if (!media.episodeLink.isNullOrEmpty()) metadata.putString(CastUtils.KEY_EPISODE_LINK, media.episodeLink)
|
||||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.episodeLink)
|
|
||||||
}
|
|
||||||
val notes: String? = media.getDescription()
|
val notes: String? = media.getDescription()
|
||||||
if (notes != null) {
|
if (notes != null) metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes)
|
||||||
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes)
|
|
||||||
}
|
|
||||||
// Default id value
|
// Default id value
|
||||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0)
|
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0)
|
||||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE)
|
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE)
|
||||||
|
@ -51,9 +39,7 @@ object MediaInfoCreator {
|
||||||
.setContentType(media.getMimeType())
|
.setContentType(media.getMimeType())
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
.setMetadata(metadata)
|
.setMetadata(metadata)
|
||||||
if (media.getDuration() > 0) {
|
if (media.getDuration() > 0) builder.setStreamDuration(media.getDuration().toLong())
|
||||||
builder.setStreamDuration(media.getDuration().toLong())
|
|
||||||
}
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,9 +52,7 @@ object MediaInfoCreator {
|
||||||
* @return [MediaInfo] object in a format proper for casting.
|
* @return [MediaInfo] object in a format proper for casting.
|
||||||
*/
|
*/
|
||||||
fun from(media: EpisodeMedia?): MediaInfo? {
|
fun from(media: EpisodeMedia?): MediaInfo? {
|
||||||
if (media == null) {
|
if (media == null) return null
|
||||||
return null
|
|
||||||
}
|
|
||||||
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC)
|
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC)
|
||||||
checkNotNull(media.episode) { "item is null" }
|
checkNotNull(media.episode) { "item is null" }
|
||||||
val feedItem = media.episode
|
val feedItem = media.episode
|
||||||
|
@ -77,35 +61,21 @@ object MediaInfoCreator {
|
||||||
val subtitle = media.getFeedTitle()
|
val subtitle = media.getFeedTitle()
|
||||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle)
|
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle)
|
||||||
|
|
||||||
|
|
||||||
val feed: Feed? = feedItem.feed
|
val feed: Feed? = feedItem.feed
|
||||||
// Manual because cast does not support embedded images
|
// Manual because cast does not support embedded images
|
||||||
val url: String = if (feedItem.imageUrl == null && feed != null) feed.imageUrl?:"" else feedItem.imageUrl?:""
|
val url: String = if (feedItem.imageUrl == null && feed != null) feed.imageUrl?:"" else feedItem.imageUrl?:""
|
||||||
if (url.isNotEmpty()) {
|
if (url.isNotEmpty()) metadata.addImage(WebImage(Uri.parse(url)))
|
||||||
metadata.addImage(WebImage(Uri.parse(url)))
|
|
||||||
}
|
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
if (media.episode?.getPubDate() != null) calendar.time = media.episode!!.getPubDate()!!
|
if (media.episode?.getPubDate() != null) calendar.time = media.episode!!.getPubDate()!!
|
||||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
if (!feed.author.isNullOrEmpty()) {
|
if (!feed.author.isNullOrEmpty()) metadata.putString(MediaMetadata.KEY_ARTIST, feed.author!!)
|
||||||
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 (!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 (!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.
|
// 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
|
// 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)
|
.setContentType(media.mimeType)
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
.setMetadata(metadata)
|
.setMetadata(metadata)
|
||||||
if (media.getDuration() > 0) {
|
if (media.getDuration() > 0) builder.setStreamDuration(media.getDuration().toLong())
|
||||||
builder.setStreamDuration(media.getDuration().toLong())
|
|
||||||
}
|
|
||||||
return builder.build()
|
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
|
# 6.5.10
|
||||||
|
|
||||||
* fixed crash when switching to a newly created queue in Queues view
|
* 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.caching=true
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx2048m
|
org.gradle.jvmargs=-Xmx2048m
|
||||||
kotlin.daemon.jvmargs=-Xmx2048m
|
#kotlin.daemon.jvmargs=-Xmx1g
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|
Loading…
Reference in New Issue