6.11.3 commit

This commit is contained in:
Xilin Jia 2024-10-16 18:55:47 +01:00
parent 61ce79f667
commit a38d7766fc
18 changed files with 187 additions and 237 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020272
versionName "6.11.2"
versionCode 3020273
versionName "6.11.3"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -54,7 +54,6 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
val eList: MutableList<Episode> = mutableListOf()
val uURL = URL(url)
// if (url.startsWith("https://youtube.com/playlist?") || url.startsWith("https://music.youtube.com/playlist?")) {
if (uURL.path.startsWith("/playlist") || uURL.path.startsWith("/playlist")) {
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch
feed_.title = playlistInfo.name

View File

@ -1436,10 +1436,10 @@ class PlaybackService : MediaLibraryService() {
Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}")
val audioIndex = if (isNetworkRestricted && prefLowQualityMedia) 0 else audioStreamsList.size - 1
val audioStream = audioStreamsList[audioIndex]
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate} forceVideo: ${media.forceVideo}")
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) {
if (media.forceVideo || 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}")
val videoStreamsList = getSortedStreamVideosList(streamInfo.videoStreams, streamInfo.videoOnlyStreams, true, true)
@ -2502,12 +2502,10 @@ class PlaybackService : MediaLibraryService() {
private fun setToFallbackSpeed(speed: Float) {
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
if (!playbackService!!.isFallbackSpeed) {
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
}
@ -2526,6 +2524,7 @@ class PlaybackService : MediaLibraryService() {
playbackService?.mPlayer?.pause(true, reinit = false)
playbackService?.isSpeedForward = false
playbackService?.isFallbackSpeed = false
(curMedia as? EpisodeMedia)?.forceVideo = false
}
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
playbackService?.mPlayer?.resume()

View File

@ -9,6 +9,9 @@ import ac.mdiq.podcini.util.showStackTrace
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import io.realm.kotlin.ext.isManaged
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.annotations.Ignore
@ -68,6 +71,9 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
// if null: unknown, will be checked
var hasEmbeddedPicture: Boolean? = null
@Ignore
var forceVideo by mutableStateOf(false)
/* Used for loading item when restoring from parcel. */
// var episodeId: Long = 0
// private set

View File

@ -82,6 +82,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val label = getLabel()
Logd(TAG, "button label: $label")
if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) {
IconButton(onClick = {
PlayActionButton(item).onClick(context)
@ -140,9 +141,9 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
fun playVideoIfNeeded(context: Context, media: Playable) {
val item = (media as? EpisodeMedia)?.episode
if (item?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
if ((media as? EpisodeMedia)?.forceVideo == true || (item?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
&& videoPlayMode != VideoMode.AUDIO_ONLY.code && videoMode != VideoMode.AUDIO_ONLY
&& media.getMediaType() == MediaType.VIDEO)
&& media.getMediaType() == MediaType.VIDEO))
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
}
}
@ -365,7 +366,6 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
}
companion object {
fun stream(context: Context, media: Playable) {
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()

View File

@ -138,11 +138,11 @@ class MainActivity : CastEnabledActivity() {
}
}
override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToTop()
// }
audioPlayer.fadePlayerToToolbar(slideOffset)
// audioPlayer.fadePlayerToToolbar(slideOffset)
}
}
@ -397,10 +397,10 @@ class MainActivity : CastEnabledActivity() {
navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0))
mainView.layoutParams = params
// val playerView = findViewById<FragmentContainerView>(R.id.playerFragment1)
val playerView = findViewById<ComposeView>(R.id.player1)
val playerParams = playerView?.layoutParams as? MarginLayoutParams
playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
playerView?.layoutParams = playerParams
// val playerView = findViewById<ComposeView>(R.id.player1)
// val playerParams = playerView?.layoutParams as? MarginLayoutParams
// playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
// playerView?.layoutParams = playerParams
audioPlayerView.visibility = if (visible) View.VISIBLE else View.GONE
}

View File

@ -117,7 +117,7 @@ class ShareReceiverActivity : AppCompatActivity() {
if (finish) activity.finish()
}
// Youtube media
(isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url) -> {
(isYoutubeURL(url) && (url.path.startsWith("/watch") || url.path.startsWith("/live"))) || isYoutubeServiceURL(url) -> {
if (log != null) upsertBlk(log) {it.type = "youtube media" }
Logd(TAG, "got youtube media")
mediaCB()

View File

@ -102,8 +102,11 @@ class VideoplayerActivity : CastEnabledActivity() {
var vmCode = 0
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
val vPol = media_.episode?.feed?.preferences?.videoModePolicy
if (vPol != null && vPol != VideoMode.NONE) vmCode = vPol.code
var vPol = media_.episode?.feed?.preferences?.videoModePolicy
if (vPol != null) {
if (vPol == VideoMode.AUDIO_ONLY && media_.forceVideo) vPol = VideoMode.WINDOW_VIEW
if (vPol != VideoMode.NONE) vmCode = vPol.code
}
}
Logd(TAG, "onCreate vmCode: $vmCode")
if (vmCode == 0) vmCode = videoPlayMode
@ -290,6 +293,7 @@ class VideoplayerActivity : CastEnabledActivity() {
when (item.itemId) {
R.id.player_switch_to_audio_only -> {
switchToAudioOnly = true
(curMedia as? EpisodeMedia)?.forceVideo = false
finish()
return true
}
@ -703,23 +707,23 @@ class VideoplayerActivity : CastEnabledActivity() {
lifecycleScope.launch {
try {
episode = withContext(Dispatchers.IO) {
var feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
if (feedItem != null) {
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
val url = feedItem.media?.downloadUrl
var episode_ = (curMedia as? EpisodeMedia)?.episodeOrFetch()
if (episode_ != null) {
val duration = episode_.media?.getDuration() ?: Int.MAX_VALUE
val url = episode_.media?.downloadUrl
val shownotesCleaner = ShownotesCleaner(requireContext())
if (url?.contains("youtube.com") == true && feedItem.description?.startsWith("Short:") == true) {
Logd(TAG, "getting extended description: ${feedItem.title}")
if (url?.contains("youtube.com") == true && episode_.description?.startsWith("Short:") == true) {
Logd(TAG, "getting extended description: ${episode_.title}")
try {
val info = feedItem.streamInfo
val info = episode_.streamInfo
if (info?.description?.content != null) {
feedItem = upsert(feedItem) { it.description = info.description?.content }
episode_ = upsert(episode_) { it.description = info.description?.content }
webviewData = shownotesCleaner.processShownotes(info.description!!.content, duration)
} else webviewData = shownotesCleaner.processShownotes(episode!!.description ?: "", duration)
} else webviewData = shownotesCleaner.processShownotes(episode_.description ?: "", duration)
} catch (e: Exception) { Logd(TAG, "StreamInfo error: ${e.message}") }
} else webviewData = shownotesCleaner.processShownotes(episode!!.description ?: "", duration)
} else webviewData = shownotesCleaner.processShownotes(episode_.description ?: "", duration)
}
feedItem
episode_
}
withContext(Dispatchers.Main) {
Logd(TAG, "load() item ${episode?.id}")
@ -798,6 +802,7 @@ class VideoplayerActivity : CastEnabledActivity() {
binding.toggleViews.setOnClickListener { (activity as VideoplayerActivity).toggleViews() }
binding.audioOnly.setOnClickListener {
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
(curMedia as? EpisodeMedia)?.forceVideo = false
(activity as? VideoplayerActivity)?.finish()
}
if (!itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData,

View File

@ -679,13 +679,14 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
if (index>=vms.size) return@LaunchedEffect
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media!!.downloadUrl!!) ?: 0
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
vm.actionButton = EpisodeActionButton.forItem(vm.episode)
vm.actionRes = vm.actionButton!!.getDrawable()
}
LaunchedEffect(key1 = status) {
if (index>=vms.size) return@LaunchedEffect
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}")
vm.actionButton = EpisodeActionButton.forItem(vms[index].episode)
vm.actionButton = EpisodeActionButton.forItem(vm.episode)
Logd(TAG, "LaunchedEffect vm.actionButton: ${vm.actionButton?.getLabel()}")
vm.actionRes = vm.actionButton!!.getDrawable()
}
// LaunchedEffect(vm.isPlayingState) {
@ -696,7 +697,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
}
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically).pointerInput(Unit) {
detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = {
vm.actionButton?.onClick(activity)
vms[index].actionButton?.onClick(activity)
})
}, contentAlignment = Alignment.Center) {
// actionRes = actionButton.getDrawable()

View File

@ -294,8 +294,7 @@ class SleepTimerDialog : DialogFragment() {
paintDial.strokeWidth = size * 0.01f
val textPos = radToPoint(i / 24.0f * 360f, size / 2 - 2.5f * padding)
paintText.textSize = 0.4f * padding
canvas.drawText(i.toString(), textPos.x.toFloat(),
textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText)
canvas.drawText(i.toString(), textPos.x.toFloat(), textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText)
}
val outer = radToPoint(i / 24.0f * 360f, size / 2 - 1.7f * padding)
val inner = radToPoint(i / 24.0f * 360f, size / 2 - 1.9f * padding)

View File

@ -1,16 +1,15 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.status
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
@ -40,7 +39,6 @@ import ac.mdiq.podcini.ui.compose.ChaptersDialog
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment.Companion
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
@ -52,9 +50,11 @@ import android.content.*
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
@ -64,11 +64,13 @@ import androidx.compose.material3.Text
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.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
@ -77,7 +79,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import coil.compose.AsyncImage
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
@ -90,15 +91,9 @@ import org.apache.commons.lang3.StringUtils
import java.text.DecimalFormat
import java.text.NumberFormat
import kotlin.math.max
import kotlin.math.min
@UnstableApi
class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
var _binding: AudioplayerFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var toolbar: MaterialToolbar
private var showPlayer1 by mutableStateOf(true)
class AudioPlayerFragment : Fragment() {
private var isCollapsed by mutableStateOf(true)
// private lateinit var controllerFuture: ListenableFuture<MediaController>
@ -122,6 +117,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var duration by mutableIntStateOf(0)
private var txtvLengtTexth by mutableStateOf("")
private var sliderValue by mutableFloatStateOf(0f)
private var sleepTimerActive by mutableStateOf(isSleepTimerActive())
private var shownotesCleaner: ShownotesCleaner? = null
@ -145,48 +141,41 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = AudioplayerFragmentBinding.inflate(inflater)
binding.root.setOnTouchListener { _: View?, _: MotionEvent? -> true } // Avoid clicks going through player to fragments below
Logd(TAG, "fragment onCreateView")
toolbar = binding.toolbar
toolbar.title = ""
toolbar.setNavigationOnClickListener {
val bottomSheet = (activity as MainActivity).bottomSheet
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
}
toolbar.setOnMenuItemClickListener(this)
controller = createHandler()
controller!!.init()
onCollaped()
binding.player1.setContent {
val composeView = ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
if (showPlayer1) PlayerUI()
else Spacer(modifier = Modifier.size(0.dp))
// Column(modifier = Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding() ) {
// if (isCollapsed) PlayerUI()
//// else Spacer(modifier = Modifier.size(0.dp))
// Toolbar()
// DetailUI(modifier = Modifier.weight(1f))
// if (!isCollapsed) PlayerUI()
//// else Spacer(modifier = Modifier.size(0.dp))
// }
Box(modifier = Modifier.fillMaxWidth().statusBarsPadding().navigationBarsPadding()) {
val aligm = if (isCollapsed) Alignment.TopCenter else Alignment.BottomCenter
PlayerUI(Modifier.align(aligm).zIndex(1f))
if (!isCollapsed) {
Column(Modifier.padding(bottom = 90.dp)) {
Toolbar()
DetailUI(modifier = Modifier)
}
}
binding.composeDetailView.setContent {
CustomTheme(requireContext()) {
DetailUI()
// if (!isCollapsed) DetailUI()
// else Spacer(modifier = Modifier.size(0.dp))
}
}
binding.player2.setContent {
CustomTheme(requireContext()) {
if (!showPlayer1) PlayerUI()
else Spacer(modifier = Modifier.size(0.dp))
}
}
// cardViewSeek = binding.cardViewSeek
(activity as MainActivity).setPlayerVisible(false)
return binding.root
return composeView
}
override fun onDestroyView() {
Logd(TAG, "Fragment destroyed")
_binding = null
controller?.release()
controller = null
// MediaController.releaseFuture(controllerFuture)
@ -195,9 +184,9 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PlayerUI() {
Column(modifier = Modifier.fillMaxWidth()) {
fun PlayerUI(modifier: Modifier) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface)) {
Text(titleText, maxLines = 1, color = textColor, style = MaterialTheme.typography.bodyMedium)
Slider(value = sliderValue, valueRange = 0f..duration.toFloat(),
// colors = SliderDefaults.colors(
@ -231,7 +220,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
}
AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(70.dp).height(70.dp).padding(start = 5.dp)
modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp)
.clickable(onClick = {
Logd(TAG, "playerUiFragment icon was clicked")
if (isCollapsed) {
@ -255,7 +244,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painter = painterResource(R.drawable.ic_playback_speed), tint = textColor,
contentDescription = "speed",
modifier = Modifier.width(48.dp).height(48.dp).clickable(onClick = {
modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = {
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
}))
Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall)
@ -264,7 +253,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painter = painterResource(R.drawable.ic_fast_rewind), tint = textColor,
contentDescription = "rewind",
modifier = Modifier.width(48.dp).height(48.dp).combinedClickable(onClick = {
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
if (controller != null && playbackService?.isServiceReady() == true) {
playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000)
}
@ -296,7 +285,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painter = painterResource(R.drawable.ic_fast_forward), tint = textColor,
contentDescription = "forward",
modifier = Modifier.width(48.dp).height(48.dp).combinedClickable(onClick = {
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
if (controller != null && playbackService?.isServiceReady() == true) {
playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000)
}
@ -317,7 +306,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
Icon(painter = painterResource(R.drawable.ic_skip_48dp), tint = textColor,
contentDescription = "rewind",
modifier = Modifier.width(48.dp).height(48.dp).combinedClickable(onClick = {
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) speedForward(speedForward)
@ -332,9 +321,72 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
@Composable
fun Toolbar() {
val media: Playable = curMedia ?: return
val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null
val textColor = MaterialTheme.colorScheme.onSurface
val mediaType = curMedia?.getMediaType()
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
Row(modifier = Modifier.fillMaxWidth().padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Icon(painter = painterResource(R.drawable.ic_arrow_down), tint = textColor, contentDescription = "Collapse", modifier = Modifier.clickable {
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
})
var homeIcon by remember { mutableIntStateOf(R.drawable.baseline_home_24)}
Icon(painter = painterResource(homeIcon), tint = textColor, contentDescription = "Home", modifier = Modifier.clickable {
homeIcon = if (showHomeText) R.drawable.ic_home else R.drawable.outline_home_24
buildHomeReaderText()
})
if (mediaType == MediaType.VIDEO) Icon(painter = painterResource(R.drawable.baseline_fullscreen_24), tint = textColor, contentDescription = "Play video",
modifier = Modifier.clickable {
if (notAudioOnly || (curMedia as? EpisodeMedia)?.forceVideo == true) {
// playPause()
} else {
(curMedia as? EpisodeMedia)?.forceVideo = true
status = PlayerStatus.STOPPED
playbackService?.mPlayer?.pause(true, reinit = true)
playbackService?.recreateMediaPlayer()
}
VideoPlayerActivityStarter(requireContext()).start()
})
if (controller != null) {
val sleepRes = if (sleepTimerActive) R.drawable.ic_sleep_off else R.drawable.ic_sleep
Icon(painter = painterResource(sleepRes), tint = textColor, contentDescription = "Sleep timer", modifier = Modifier.clickable {
SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
})
}
if (currentMedia is EpisodeMedia) Icon(painter = painterResource(R.drawable.ic_feed), tint = textColor, contentDescription = "Open podcast",
modifier = Modifier.clickable {
if (feedItem?.feedId != null) {
val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId!!)
startActivity(intent)
}
})
Icon(painter = painterResource(R.drawable.ic_share), tint = textColor, contentDescription = "Share", modifier = Modifier.clickable {
if (currentItem != null) {
val shareDialog: ShareDialog = ShareDialog.newInstance(currentItem!!)
shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
}
})
Icon(painter = painterResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable {
val notes = if (showHomeText) readerhtml else feedItem?.description
if (!notes.isNullOrEmpty()) {
val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
val context = requireContext()
val intent = ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(shareText)
.setChooserTitle(R.string.share_notes_label)
.createChooserIntent()
context.startActivity(intent)
}
})
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DetailUI() {
fun DetailUI(modifier: Modifier) {
var showChooseRatingDialog by remember { mutableStateOf(false) }
if (showChooseRatingDialog) ChooseRatingDialog(listOf(currentItem!!)) {
showChooseRatingDialog = false
@ -343,7 +395,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (showChaptersDialog) ChaptersDialog(media = currentMedia!!, onDismissRequest = {showChaptersDialog = false})
val scrollState = rememberScrollState()
Column(modifier = Modifier.fillMaxWidth().verticalScroll(scrollState)) {
Column(modifier = modifier.fillMaxWidth().verticalScroll(scrollState)) {
val textColor = MaterialTheme.colorScheme.onSurface
fun copyText(text: String): Boolean {
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
@ -647,7 +699,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
// if (isCollapsed) {
isCollapsed = false
if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext())
showPlayer1 = false
// showPlayer1 = false
if (currentMedia != null) updateUi(currentMedia!!)
setIsShowPlay(isShowPlay)
updateDetails()
@ -657,7 +709,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun onCollaped() {
Logd(TAG, "onCollaped()")
isCollapsed = true
showPlayer1 = true
// showPlayer1 = true
if (currentMedia != null) updateUi(currentMedia!!)
setIsShowPlay(isShowPlay)
}
@ -703,7 +755,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val item = (currentMedia as? EpisodeMedia)?.episodeOrFetch()
if (item != null) setItem(item)
setChapterDividers()
setupOptionsMenu()
sleepTimerActive = isSleepTimerActive()
if (currentMedia != null) updateUi(currentMedia!!)
// TODO: disable for now
// if (!includingChapters) loadMediaInfo(true)
@ -850,7 +902,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
is FlowEvent.RatingEvent -> onRatingEvent(event)
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
// is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) setupOptionsMenu()
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) sleepTimerActive = isSleepTimerActive()
is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event)
is FlowEvent.SpeedChangedEvent -> updatePlaybackSpeedButton(event)
else -> {}
@ -866,87 +918,11 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
private fun setupOptionsMenu() {
if (toolbar.menu.size() == 0) toolbar.inflateMenu(R.menu.mediaplayer)
val isEpisodeMedia = currentMedia is EpisodeMedia
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
// val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episodeOrFetch() else null
// EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
val mediaType = curMedia?.getMediaType()
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO && notAudioOnly)
if (controller != null) {
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
}
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
}
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
val media: Playable = curMedia ?: return false
val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null
// if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
val itemId = menuItem.itemId
when (itemId) {
R.id.show_home_reader_view -> {
if (showHomeText) menuItem.setIcon(R.drawable.ic_home)
else menuItem.setIcon(R.drawable.outline_home_24)
buildHomeReaderText()
}
R.id.show_video -> {
playPause()
VideoPlayerActivityStarter(requireContext()).start()
}
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
R.id.open_feed_item -> {
if (feedItem?.feedId != null) {
val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId!!)
startActivity(intent)
}
}
R.id.share_notes -> {
val notes = if (showHomeText) readerhtml else feedItem?.description
if (!notes.isNullOrEmpty()) {
val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
val context = requireContext()
val intent = ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(shareText)
.setChooserTitle(R.string.share_notes_label)
.createChooserIntent()
context.startActivity(intent)
}
}
R.id.share_item -> {
if (currentItem != null) {
val shareDialog: ShareDialog = ShareDialog.newInstance(currentItem!!)
shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
}
}
else -> return false
}
return true
}
// fun scrollToTop() {
//// binding.itemDescriptionFragment.scrollTo(0, 0)
// savePreference()
// }
fun fadePlayerToToolbar(slideOffset: Float) {
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
val player = binding.player1
player.alpha = 1 - playerFadeProgress
player.visibility = if (playerFadeProgress > 0.99f) View.GONE else View.VISIBLE
val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat()
toolbar.setAlpha(toolbarFadeProgress)
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.GONE else View.VISIBLE
}
companion object {
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
var media3Controller: MediaController? = null

View File

@ -32,6 +32,8 @@ import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.*
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion
import ac.mdiq.podcini.ui.compose.ChaptersDialog
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
@ -456,26 +458,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
else -> txtvSize = ""
}
// val imgLocFB = ImageResourceUtils.getFallbackImageLocation(episode!!)
// val imageLoader = imgvCover.context.imageLoader
// val imageRequest = ImageRequest.Builder(requireContext())
// .data(episode!!.imageLocation)
// .placeholder(R.color.light_gray)
// .listener(object : ImageRequest.Listener {
// override fun onError(request: ImageRequest, result: ErrorResult) {
// val fallbackImageRequest = ImageRequest.Builder(requireContext())
// .data(imgLocFB)
// .setHeader("User-Agent", "Mozilla/5.0")
// .error(R.mipmap.ic_launcher)
// .target(imgvCover)
// .build()
// imageLoader.enqueue(fallbackImageRequest)
// }
// })
// .target(imgvCover)
// .build()
// imageLoader.enqueue(imageRequest)
updateButtons()
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M14.6,10.26v1.31L17,9.33 14.6,7.1v1.28c-2.33,0.32 -3.26,1.92 -3.6,3.52 0.83,-1.13 1.93,-1.64 3.6,-1.64zM16,23L6,23c-1.1,0 -2,-0.9 -2,-2L4,5h2v16h10v2zM18,1h-8c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L20,3c0,-1.1 -0.9,-2 -2,-2zM18,16h-8L10,4h8v12z"/>
</vector>

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/audioplayer_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/player1"
android:layout_width="match_parent"
android:layout_height="@dimen/external_player_height"
android:elevation="8dp"
android:outlineProvider="none"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="@drawable/ic_arrow_down" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeDetailView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginBottom="12dp"/>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/player2"
android:layout_width="match_parent"
android:layout_height="@dimen/external_player_height"/>
</LinearLayout>
</LinearLayout>

View File

@ -7,8 +7,8 @@ buildscript {
ext.kotlin_version = "$libs.versions.kotlin"
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:8.5.2'
classpath 'org.codehaus.groovy:groovy-xml:3.0.19'
classpath libs.gradle
classpath libs.groovy.xml
}
}

View File

@ -1,3 +1,14 @@
# 6.11.3
* supports Youtube live episodes received from share
* fixed info not showing when playing video in window mode
* AudioPlayer is fully in Compose, fixed the issue of top menu sometimes not shown
* if you have podcast set to AudioOnly, you can tap on the square icon on the top bar of PlayerDetailed to force play video
* this will re-construct the media item for the current episode to include video and plays audio-video together
* it continues this way even after you close the video view and only listen
* during this mode, you can switch between video and audio and the play is uninterrupted
* it will resume playing audio only when you switch episodes and comeback to it
# 6.11.2
* fixed PlayerDetailed view not showing full info on Youtube media

View File

@ -0,0 +1,10 @@
Version 6.11.3
* supports Youtube live episodes received from share
* fixed info not showing when playing video in window mode
* AudioPlayer is fully in Compose, fixed the issue of top menu sometimes not shown
* if you have podcast set to AudioOnly, you can tap on the square icon on the top bar of PlayerDetailed to force play video
* this will re-construct the media item for the current episode to include video and plays audio-video together
* it continues this way even after you close the video view and only listen
* during this mode, you can switch between video and audio and the play is uninterrupted
* it will resume playing audio only when you switch episodes and comeback to it

View File

@ -22,7 +22,9 @@ fontawesomeTypeface = "5.13.3.0-kotlin"
fyydlin = "v0.5.0"
googleMaterialTypeface = "4.0.0.3-kotlin"
googleMaterialTypefaceOutlined = "4.0.0.2-kotlin"
gradle = "8.5.2"
gridlayout = "1.0.0"
groovyXml = "3.0.19"
iconicsCore = "5.5.0-b01"
iconicsViews = "5.5.0-b01"
javaxInject = "1"
@ -123,6 +125,8 @@ fontawesome-typeface = { module = "com.mikepenz:fontawesome-typeface", version.r
fyydlin = { module = "com.github.mfietz:fyydlin", version.ref = "fyydlin" }
google-material-typeface-outlined = { module = "com.mikepenz:google-material-typeface-outlined", version.ref = "googleMaterialTypefaceOutlined" }
google-material-typeface = { module = "com.mikepenz:google-material-typeface", version.ref = "googleMaterialTypeface" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
groovy-xml = { module = "org.codehaus.groovy:groovy-xml", version.ref = "groovyXml" }
iconics-views = { module = "com.mikepenz:iconics-views", version.ref = "iconicsViews" }
iconics-core = { module = "com.mikepenz:iconics-core", version.ref = "iconicsCore" }
javax-inject = { module = "javax.inject:javax.inject", version.ref = "javaxInject" }