mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
YakBak in PictureInPicture mode
New audio player UI for YakBak
This commit is contained in:
+10
-8
@@ -67,10 +67,11 @@ fun GetVideoController(
|
||||
scope.launch {
|
||||
Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}")
|
||||
PlaybackServiceClient.prepareController(
|
||||
controllerId,
|
||||
mediaItem.src.videoUri,
|
||||
mediaItem.src.proxyPort,
|
||||
context,
|
||||
mediaControllerState = controllerId,
|
||||
videoUri = mediaItem.src.videoUri,
|
||||
proxyPort = mediaItem.src.proxyPort,
|
||||
keepPlaying = mediaItem.src.keepPlaying,
|
||||
context = context,
|
||||
) { controllerId ->
|
||||
scope.launch(Dispatchers.Main) {
|
||||
// checks if the player is still active after requesting to load
|
||||
@@ -174,10 +175,11 @@ fun GetVideoController(
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Log.d("PlaybackService", "Preparing Video from Resume ${controllerId.id} ${mediaItem.src.videoUri} ")
|
||||
PlaybackServiceClient.prepareController(
|
||||
controllerId,
|
||||
mediaItem.src.videoUri,
|
||||
mediaItem.src.proxyPort,
|
||||
context,
|
||||
mediaControllerState = controllerId,
|
||||
videoUri = mediaItem.src.videoUri,
|
||||
proxyPort = mediaItem.src.proxyPort,
|
||||
keepPlaying = mediaItem.src.keepPlaying,
|
||||
context = context,
|
||||
) { controllerId ->
|
||||
scope.launch(Dispatchers.Main) {
|
||||
// checks if the player is still active after requesting to load
|
||||
|
||||
+9
-2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -49,7 +50,6 @@ fun RenderVideoPlayer(
|
||||
thumbData: VideoThumb?,
|
||||
showControls: Boolean = true,
|
||||
contentScale: ContentScale,
|
||||
waveform: WaveformData? = null,
|
||||
borderModifier: Modifier,
|
||||
videoModifier: Modifier,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
@@ -64,6 +64,13 @@ fun RenderVideoPlayer(
|
||||
factory = { context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controllerState.controller
|
||||
// if we alrady know the size of the frame, this forces the player to stay in the size
|
||||
layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
|
||||
setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
@@ -101,7 +108,7 @@ fun RenderVideoPlayer(
|
||||
},
|
||||
)
|
||||
|
||||
waveform?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) }
|
||||
mediaItem.src.waveformData?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) }
|
||||
|
||||
if (showControls) {
|
||||
RenderControlButtons(
|
||||
|
||||
+9
-8
@@ -55,14 +55,16 @@ fun VideoViewInner(
|
||||
val muted = remember(videoUri) { DEFAULT_MUTED_SETTING.value }
|
||||
|
||||
GetMediaItem(
|
||||
videoUri,
|
||||
title,
|
||||
artworkUri,
|
||||
authorName,
|
||||
nostrUriCallback,
|
||||
mimeType,
|
||||
aspectRatio,
|
||||
videoUri = videoUri,
|
||||
title = title,
|
||||
artworkUri = artworkUri,
|
||||
authorName = authorName,
|
||||
callbackUri = nostrUriCallback,
|
||||
mimeType = mimeType,
|
||||
aspectRatio = aspectRatio,
|
||||
proxyPort = accountViewModel.proxyPortFor(videoUri),
|
||||
keepPlaying = true,
|
||||
waveformData = waveform,
|
||||
) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
@@ -76,7 +78,6 @@ fun VideoViewInner(
|
||||
thumbData = thumb,
|
||||
showControls = showControls,
|
||||
contentScale = contentScale,
|
||||
waveform = waveform,
|
||||
borderModifier = borderModifier,
|
||||
videoModifier = videoModifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
|
||||
+5
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
|
||||
val mediaItemCache = MediaItemCache()
|
||||
|
||||
@@ -37,6 +38,8 @@ fun GetMediaItem(
|
||||
mimeType: String? = null,
|
||||
aspectRatio: Float? = null,
|
||||
proxyPort: Int? = null,
|
||||
keepPlaying: Boolean = false,
|
||||
waveformData: WaveformData? = null,
|
||||
inner: @Composable (LoadedMediaItem) -> Unit,
|
||||
) {
|
||||
val data =
|
||||
@@ -50,6 +53,8 @@ fun GetMediaItem(
|
||||
mimeType = mimeType,
|
||||
aspectRatio = aspectRatio,
|
||||
proxyPort = proxyPort,
|
||||
keepPlaying = keepPlaying,
|
||||
waveformData = waveformData,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.media3.common.MediaItem
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
|
||||
@Immutable
|
||||
data class MediaItemData(
|
||||
@@ -33,6 +34,8 @@ data class MediaItemData(
|
||||
val mimeType: String? = null,
|
||||
val aspectRatio: Float? = null,
|
||||
val proxyPort: Int? = null,
|
||||
val keepPlaying: Boolean = true,
|
||||
val waveformData: WaveformData? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.playback.pip
|
||||
|
||||
import android.R.attr.data
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@@ -58,6 +60,8 @@ class IntentExtras {
|
||||
mimeType = intent.getString("mimeType"),
|
||||
aspectRatio = if (ratio > 0) ratio else null,
|
||||
proxyPort = if (port > 0) port else null,
|
||||
keepPlaying = intent.getBoolean("keepPlaying", true),
|
||||
waveformData = intent.getFloatArray("wavefrontData")?.toList()?.let { WaveformData(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,6 +78,8 @@ class IntentExtras {
|
||||
data.mimeType?.let { putString("mimeType", it) }
|
||||
data.aspectRatio?.let { putFloat("aspectRatio", it) }
|
||||
data.proxyPort?.let { putInt("proxyPort", it) }
|
||||
data.keepPlaying.let { putBoolean("keepPlaying", it) }
|
||||
data.waveformData?.let { putFloatArray("wavefrontData", it.wave.toFloatArray()) }
|
||||
|
||||
bounds?.let { putInt("boundLeft", it.left) }
|
||||
bounds?.let { putInt("boundRight", it.right) }
|
||||
|
||||
+13
-2
@@ -24,6 +24,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -42,8 +43,11 @@ import androidx.media3.ui.PlayerView
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
||||
|
||||
@Composable
|
||||
fun PipVideo() {
|
||||
@@ -69,7 +73,7 @@ fun PipVideo() {
|
||||
videoData?.let {
|
||||
GetMediaItem(it) { mediaItem ->
|
||||
GetVideoController(mediaItem, false) { controller ->
|
||||
PipVideo(controller)
|
||||
PipVideo(controller, it.waveformData)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +81,10 @@ fun PipVideo() {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun PipVideo(controller: MediaControllerState) {
|
||||
fun PipVideo(
|
||||
controller: MediaControllerState,
|
||||
waveformData: WaveformData?,
|
||||
) {
|
||||
DisposableEffect(controller) {
|
||||
BackgroundMedia.switchKeepPlaying(controller)
|
||||
onDispose {
|
||||
@@ -117,5 +124,9 @@ fun PipVideo(controller: MediaControllerState) {
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
waveformData?.let { Waveform(it, controller, Modifier) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -56,7 +56,7 @@ class SessionListener(
|
||||
class MediaSessionPool(
|
||||
val exoPlayerPool: ExoPlayerPool,
|
||||
val okHttpClient: OkHttpClient,
|
||||
val reset: (MediaSession) -> Unit,
|
||||
val reset: (MediaSession, Boolean) -> Unit,
|
||||
) {
|
||||
val globalCallback = MediaSessionCallback(this)
|
||||
var lastCleanup = TimeUtils.now()
|
||||
@@ -65,7 +65,7 @@ class MediaSessionPool(
|
||||
private val playingMap = mutableMapOf<String, SessionListener>()
|
||||
|
||||
private val cache =
|
||||
object : LruCache<String, SessionListener>(SimultaneousPlaybackCalculator.max()) { // up to 10 videos in the screen at the same time
|
||||
object : LruCache<String, SessionListener>(10) { // up to 10 videos in the screen at the same time
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: String?,
|
||||
@@ -87,6 +87,7 @@ class MediaSessionPool(
|
||||
@OptIn(UnstableApi::class)
|
||||
fun newSession(
|
||||
id: String,
|
||||
keepPlaying: Boolean,
|
||||
context: Context,
|
||||
): MediaSession {
|
||||
val mediaSession =
|
||||
@@ -107,7 +108,7 @@ class MediaSessionPool(
|
||||
|
||||
mediaSession.player.addListener(listener)
|
||||
|
||||
reset(mediaSession)
|
||||
reset(mediaSession, keepPlaying)
|
||||
|
||||
cache.put(mediaSession.id, SessionListener(mediaSession, listener))
|
||||
|
||||
@@ -167,6 +168,7 @@ class MediaSessionPool(
|
||||
|
||||
fun getSession(
|
||||
id: String,
|
||||
keepPlaying: Boolean,
|
||||
context: Context,
|
||||
): MediaSession {
|
||||
val existingSession = playingMap.get(id) ?: cache.get(id)
|
||||
@@ -174,7 +176,7 @@ class MediaSessionPool(
|
||||
return existingSession.session
|
||||
}
|
||||
|
||||
return newSession(id, context)
|
||||
return newSession(id, keepPlaying, context)
|
||||
}
|
||||
|
||||
fun playingContent() = playingMap.values
|
||||
|
||||
+4
-3
@@ -44,9 +44,9 @@ class PlaybackService : MediaSessionService() {
|
||||
MediaSessionPool(
|
||||
ExoPlayerPool(ExoPlayerBuilder(okHttp)),
|
||||
okHttpClient = okHttp,
|
||||
reset = { session ->
|
||||
reset = { session, keepPlaying ->
|
||||
(session.player as ExoPlayer).apply {
|
||||
repeatMode = Player.REPEAT_MODE_ONE
|
||||
repeatMode = if (keepPlaying) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||
volume = 0f
|
||||
}
|
||||
@@ -145,7 +145,8 @@ class PlaybackService : MediaSessionService() {
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
val id = controllerInfo.connectionHints.getString("id") ?: return null
|
||||
val proxyPort = controllerInfo.connectionHints.getInt("proxyPort")
|
||||
val keepPlaying = controllerInfo.connectionHints.getBoolean("keepPlaying", true)
|
||||
val manager = lazyPool(proxyPort)
|
||||
return manager.getSession(id, applicationContext)
|
||||
return manager.getSession(id, keepPlaying, applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -60,6 +60,7 @@ object PlaybackServiceClient {
|
||||
mediaControllerState: MediaControllerState,
|
||||
videoUri: String,
|
||||
proxyPort: Int? = 0,
|
||||
keepPlaying: Boolean = true,
|
||||
context: Context,
|
||||
onReady: (MediaControllerState) -> Unit,
|
||||
) {
|
||||
@@ -73,6 +74,7 @@ object PlaybackServiceClient {
|
||||
// link the id with the client's id to make sure it can return the
|
||||
// same session on background media.
|
||||
putString("id", mediaControllerState.id)
|
||||
putBoolean("keepPlaying", keepPlaying)
|
||||
proxyPort?.let {
|
||||
putInt("proxyPort", it)
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ private fun RenderNoteRow(
|
||||
is VideoNormalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
|
||||
is VideoShortEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
|
||||
is PictureEvent -> PictureDisplay(baseNote, true, ContentScale.FillWidth, PaddingValues(vertical = 5.dp), backgroundColor, accountViewModel, nav)
|
||||
is BaseVoiceEvent -> RenderVoiceTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
is BaseVoiceEvent -> RenderVoiceTrack(baseNote, accountViewModel, nav)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
|
||||
is CommunityPostApprovalEvent -> {
|
||||
RenderPostApproval(
|
||||
|
||||
@@ -20,22 +20,56 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PauseCircleOutline
|
||||
import androidx.compose.material.icons.filled.PlayCircleOutline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.controls.RenderControlButtons
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia.isPlaying
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
@@ -43,23 +77,28 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
@Composable
|
||||
fun RenderVoiceTrack(
|
||||
note: Note,
|
||||
contentScale: ContentScale,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? BaseVoiceEvent ?: return
|
||||
|
||||
VoiceHeader(noteEvent, note, contentScale, accountViewModel, nav)
|
||||
VoiceHeader(noteEvent, note, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VoiceHeader(
|
||||
noteEvent: BaseVoiceEvent,
|
||||
note: Note,
|
||||
contentScale: ContentScale,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val media =
|
||||
remember(noteEvent) {
|
||||
noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url
|
||||
}
|
||||
|
||||
if (media == null) return
|
||||
|
||||
val waveform =
|
||||
remember(noteEvent) {
|
||||
noteEvent
|
||||
@@ -68,32 +107,40 @@ fun VoiceHeader(
|
||||
?.waveform
|
||||
?.let { WaveformData(it) }
|
||||
}
|
||||
val media =
|
||||
remember(noteEvent) {
|
||||
noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url
|
||||
}
|
||||
|
||||
if (media == null) return
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(top = 5.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
VideoView(
|
||||
GetMediaItem(
|
||||
videoUri = media,
|
||||
mimeType = null,
|
||||
waveform = waveform,
|
||||
title = noteEvent.subject(),
|
||||
artworkUri = null,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
roundedCorner = true,
|
||||
contentScale = contentScale,
|
||||
accountViewModel = accountViewModel,
|
||||
nostrUriCallback = note.toNostrUri(),
|
||||
)
|
||||
callbackUri = callbackUri,
|
||||
mimeType = null,
|
||||
aspectRatio = null,
|
||||
proxyPort = accountViewModel.proxyPortFor(media),
|
||||
keepPlaying = false,
|
||||
waveformData = waveform,
|
||||
) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
muted = false,
|
||||
) { controller ->
|
||||
RenderVoicePlayer(
|
||||
mediaItem = mediaItem,
|
||||
controllerState = controller,
|
||||
waveform = waveform,
|
||||
borderModifier = MaterialTheme.colorScheme.imageModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav)
|
||||
@@ -101,3 +148,119 @@ fun VoiceHeader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(UnstableApi::class)
|
||||
fun RenderVoicePlayer(
|
||||
mediaItem: LoadedMediaItem,
|
||||
controllerState: MediaControllerState,
|
||||
waveform: WaveformData? = null,
|
||||
borderModifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val controllerVisible = remember(controllerState) { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = borderModifier) {
|
||||
AndroidView(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
factory = { context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controllerState.controller
|
||||
// if we alrady know the size of the frame, this forces the player to stay in the size
|
||||
layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
|
||||
controllerAutoShow = false
|
||||
useController = true
|
||||
hideController()
|
||||
|
||||
setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visible ->
|
||||
controllerVisible.value = visible == View.VISIBLE
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
PlayPauseButton(controllerState)
|
||||
waveform?.let { Waveform(it, controllerState, Modifier) }
|
||||
}
|
||||
|
||||
RenderControlButtons(
|
||||
mediaItem.src,
|
||||
controllerState,
|
||||
controllerVisible,
|
||||
Modifier.align(Alignment.TopEnd),
|
||||
accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayPauseButton(controllerState: MediaControllerState) {
|
||||
val controller = controllerState.controller
|
||||
var isPlaying by remember(controller) {
|
||||
mutableStateOf(controllerState.isPlaying())
|
||||
}
|
||||
|
||||
if (controller != null) {
|
||||
val view = LocalView.current
|
||||
// Keeps the screen on while playing and viewing videos.
|
||||
DisposableEffect(key1 = controller, key2 = view) {
|
||||
val listener =
|
||||
object : Player.Listener {
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
// doesn't consider the mutex because the screen can turn off if the video
|
||||
// being played in the mutex is not visible.
|
||||
if (view.keepScreenOn != playing) {
|
||||
view.keepScreenOn = playing
|
||||
}
|
||||
isPlaying = playing
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
if (view.keepScreenOn) {
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
isPlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
controller.addListener(listener)
|
||||
onDispose {
|
||||
controller.removeListener(listener)
|
||||
listener.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// Play/Pause Button
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (controller.isPlaying) {
|
||||
controller.pause()
|
||||
} else {
|
||||
controller.play()
|
||||
}
|
||||
},
|
||||
modifier = Size75Modifier,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isPlaying) Icons.Default.PauseCircleOutline else Icons.Default.PlayCircleOutline,
|
||||
contentDescription = if (isPlaying) stringResource(R.string.pause) else stringResource(R.string.play),
|
||||
modifier = Size50Modifier,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -552,7 +552,7 @@ private fun FullBleedNoteCompose(
|
||||
} else if (noteEvent is PictureEvent) {
|
||||
PictureDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, PaddingValues(vertical = Size5dp), backgroundColor, accountViewModel = accountViewModel, nav)
|
||||
} else if (noteEvent is BaseVoiceEvent) {
|
||||
VoiceHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
VoiceHeader(noteEvent, baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
|
||||
@@ -165,6 +165,7 @@ val Size39Modifier = Modifier.size(39.dp)
|
||||
val Size40Modifier = Modifier.size(40.dp)
|
||||
val Size50Modifier = Modifier.size(50.dp)
|
||||
val Size55Modifier = Modifier.size(55.dp)
|
||||
val Size75Modifier = Modifier.size(75.dp)
|
||||
|
||||
val TinyBorders = Modifier.padding(2.dp)
|
||||
val NoSoTinyBorders = Modifier.padding(start = 5.dp, end = 5.dp, top = 2.dp, bottom = 2.dp)
|
||||
@@ -351,3 +352,7 @@ val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder)
|
||||
val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp)
|
||||
|
||||
val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20))
|
||||
|
||||
val MaxWidthPaddingTop5dp = Modifier.fillMaxWidth().padding(top = 5.dp)
|
||||
|
||||
val VoiceHeightModifier = Modifier.fillMaxWidth().height(100.dp)
|
||||
|
||||
@@ -1229,4 +1229,6 @@
|
||||
|
||||
<string name="dont_translate_from">Don\'t Translate From</string>
|
||||
<string name="dont_translate_from_description">Languages shown here will not be translated. Select a language to remove it and have it translated again.</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="play">Play</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user