diff --git a/app/build.gradle b/app/build.gradle index b166b44a..73b28957 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020272 - versionName "6.11.2" + versionCode 3020273 + versionName "6.11.3" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt index bf599c57..e2a7cf2b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt @@ -54,7 +54,6 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) val eList: MutableList = 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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index cdac8b15..a4885558 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index d75f82f6..ad58bf65 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt index 28d1626c..ad765d86 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index 302e5f95..b8493bdf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -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(R.id.playerFragment1) - val playerView = findViewById(R.id.player1) - val playerParams = playerView?.layoutParams as? MarginLayoutParams - playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0) - playerView?.layoutParams = playerParams +// val playerView = findViewById(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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt index 9a767741..708e42e0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index b0263005..b6ca9033 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -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, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index add8e6e9..077d82cb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -679,13 +679,14 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, 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, } 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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt index 423986ad..d881fe58 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 9f030b20..50d325db 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -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 @@ -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 { - CustomTheme(requireContext()) { - if (showPlayer1) PlayerUI() - else Spacer(modifier = Modifier.size(0.dp)) + val composeView = ComposeView(requireContext()).apply { + setContent { + CustomTheme(requireContext()) { +// 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()) { - val textColor = MaterialTheme.colorScheme.onSurface + 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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 671b7f60..b0361ed4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -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() } diff --git a/app/src/main/res/drawable/baseline_offline_share_24.xml b/app/src/main/res/drawable/baseline_offline_share_24.xml new file mode 100644 index 00000000..e064e4b5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_offline_share_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml deleted file mode 100644 index 21615f3c..00000000 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/build.gradle b/build.gradle index 08e3bb7a..7f481cd4 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } } diff --git a/changelog.md b/changelog.md index 1521f823..424eb891 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/fastlane/metadata/android/en-US/changelogs/3020273.txt b/fastlane/metadata/android/en-US/changelogs/3020273.txt new file mode 100644 index 00000000..84415e76 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020273.txt @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 481fc360..76d5aa84 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }