mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-10 20:44:04 +02:00
Adds a waveform to audio files
This commit is contained in:
@@ -182,6 +182,9 @@ dependencies {
|
|||||||
// GeoHash
|
// GeoHash
|
||||||
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
|
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
|
||||||
|
|
||||||
|
// Waveform visualizer
|
||||||
|
implementation 'com.github.lincollincol:compose-audiowaveform:1.1.1'
|
||||||
|
|
||||||
// Video compression lib
|
// Video compression lib
|
||||||
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
||||||
// Image compression lib
|
// Image compression lib
|
||||||
|
@@ -0,0 +1,160 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.coerceIn
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.linc.audiowaveform.model.AmplitudeType
|
||||||
|
import com.linc.audiowaveform.model.WaveformAlignment
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private val MinSpikeWidthDp: Dp = 1.dp
|
||||||
|
private val MaxSpikeWidthDp: Dp = 24.dp
|
||||||
|
private val MinSpikePaddingDp: Dp = 0.dp
|
||||||
|
private val MaxSpikePaddingDp: Dp = 12.dp
|
||||||
|
private val MinSpikeRadiusDp: Dp = 0.dp
|
||||||
|
private val MaxSpikeRadiusDp: Dp = 12.dp
|
||||||
|
|
||||||
|
private const val MinProgress: Float = 0F
|
||||||
|
private const val MaxProgress: Float = 1F
|
||||||
|
|
||||||
|
private const val MinSpikeHeight: Float = 1F
|
||||||
|
private const val DefaultGraphicsLayerAlpha: Float = 0.99F
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AudioWaveformReadOnly(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: DrawStyle = Fill,
|
||||||
|
waveformBrush: Brush = SolidColor(Color.White),
|
||||||
|
progressBrush: Brush = SolidColor(Color.Blue),
|
||||||
|
waveformAlignment: WaveformAlignment = WaveformAlignment.Center,
|
||||||
|
amplitudeType: AmplitudeType = AmplitudeType.Avg,
|
||||||
|
onProgressChangeFinished: (() -> Unit)? = null,
|
||||||
|
spikeAnimationSpec: AnimationSpec<Float> = tween(500),
|
||||||
|
spikeWidth: Dp = 3.dp,
|
||||||
|
spikeRadius: Dp = 2.dp,
|
||||||
|
spikePadding: Dp = 2.dp,
|
||||||
|
progress: Float = 0F,
|
||||||
|
amplitudes: List<Int>,
|
||||||
|
onProgressChange: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
val backgroundColor = MaterialTheme.colors.background
|
||||||
|
val _progress = remember(progress) { progress.coerceIn(MinProgress, MaxProgress) }
|
||||||
|
val _spikeWidth = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
|
||||||
|
val _spikePadding = remember(spikePadding) { spikePadding.coerceIn(MinSpikePaddingDp, MaxSpikePaddingDp) }
|
||||||
|
val _spikeRadius = remember(spikeRadius) { spikeRadius.coerceIn(MinSpikeRadiusDp, MaxSpikeRadiusDp) }
|
||||||
|
val _spikeTotalWidth = remember(spikeWidth, spikePadding) { _spikeWidth + _spikePadding }
|
||||||
|
var canvasSize by remember { mutableStateOf(Size(0f, 0f)) }
|
||||||
|
var spikes by remember { mutableStateOf(0F) }
|
||||||
|
val spikesAmplitudes = remember(amplitudes, spikes, amplitudeType) {
|
||||||
|
amplitudes.toDrawableAmplitudes(
|
||||||
|
amplitudeType = amplitudeType,
|
||||||
|
spikes = spikes.toInt(),
|
||||||
|
minHeight = MinSpikeHeight,
|
||||||
|
maxHeight = canvasSize.height.coerceAtLeast(MinSpikeHeight)
|
||||||
|
)
|
||||||
|
}.map { animateFloatAsState(it, spikeAnimationSpec).value }
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.requiredHeight(48.dp)
|
||||||
|
.graphicsLayer(alpha = DefaultGraphicsLayerAlpha)
|
||||||
|
.then(modifier)
|
||||||
|
) {
|
||||||
|
canvasSize = size
|
||||||
|
spikes = size.width / _spikeTotalWidth.toPx()
|
||||||
|
spikesAmplitudes.forEachIndexed { index, amplitude ->
|
||||||
|
drawRoundRect(
|
||||||
|
brush = waveformBrush,
|
||||||
|
topLeft = Offset(
|
||||||
|
x = index * _spikeTotalWidth.toPx(),
|
||||||
|
y = when (waveformAlignment) {
|
||||||
|
WaveformAlignment.Top -> 0F
|
||||||
|
WaveformAlignment.Bottom -> size.height - amplitude
|
||||||
|
WaveformAlignment.Center -> size.height / 2F - amplitude / 2F
|
||||||
|
}
|
||||||
|
),
|
||||||
|
size = Size(
|
||||||
|
width = _spikeWidth.toPx(),
|
||||||
|
height = amplitude
|
||||||
|
),
|
||||||
|
cornerRadius = CornerRadius(_spikeRadius.toPx(), _spikeRadius.toPx()),
|
||||||
|
style = style
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
brush = progressBrush,
|
||||||
|
size = Size(
|
||||||
|
width = _progress * size.width,
|
||||||
|
height = size.height
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.SrcAtop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Int>.toDrawableAmplitudes(
|
||||||
|
amplitudeType: AmplitudeType,
|
||||||
|
spikes: Int,
|
||||||
|
minHeight: Float,
|
||||||
|
maxHeight: Float
|
||||||
|
): List<Float> {
|
||||||
|
val amplitudes = map(Int::toFloat)
|
||||||
|
if (amplitudes.isEmpty() || spikes == 0) {
|
||||||
|
return List(spikes) { minHeight }
|
||||||
|
}
|
||||||
|
val transform = { data: List<Float> ->
|
||||||
|
when (amplitudeType) {
|
||||||
|
AmplitudeType.Avg -> data.average()
|
||||||
|
AmplitudeType.Max -> data.max()
|
||||||
|
AmplitudeType.Min -> data.min()
|
||||||
|
}.toFloat().coerceIn(minHeight, maxHeight)
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
spikes > amplitudes.count() -> amplitudes.fillToSize(spikes, transform)
|
||||||
|
else -> amplitudes.chunkToSize(spikes, transform)
|
||||||
|
}.normalize(minHeight, maxHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> Iterable<T>.fillToSize(size: Int, transform: (List<T>) -> T): List<T> {
|
||||||
|
val capacity = ceil(size.safeDiv(count())).roundToInt()
|
||||||
|
return map { data -> List(capacity) { data } }.flatten().chunkToSize(size, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> Iterable<T>.chunkToSize(size: Int, transform: (List<T>) -> T): List<T> {
|
||||||
|
val chunkSize = count() / size
|
||||||
|
val remainder = count() % size
|
||||||
|
val remainderIndex = ceil(count().safeDiv(remainder)).roundToInt()
|
||||||
|
val chunkIteration = filterIndexed { index, _ ->
|
||||||
|
remainderIndex == 0 || index % remainderIndex != 0
|
||||||
|
}.chunked(chunkSize, transform)
|
||||||
|
return when (size) {
|
||||||
|
chunkIteration.count() -> chunkIteration
|
||||||
|
else -> chunkIteration.chunkToSize(size, transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Iterable<Float>.normalize(min: Float, max: Float): List<Float> {
|
||||||
|
return map { (max - min) * ((it - min()) / (max() - min())) + min }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.safeDiv(value: Int): Float {
|
||||||
|
return if (value == 0) return 0F else this / value.toFloat()
|
||||||
|
}
|
@@ -9,6 +9,8 @@ import android.view.ViewGroup
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -34,6 +36,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.LayoutCoordinates
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@@ -54,6 +58,7 @@ import androidx.media3.ui.AspectRatioFrameLayout
|
|||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.linc.audiowaveform.infiniteLinearGradient
|
||||||
import com.vitorpamplona.amethyst.PlaybackClientController
|
import com.vitorpamplona.amethyst.PlaybackClientController
|
||||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||||
@@ -66,8 +71,11 @@ import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -137,6 +145,7 @@ fun VideoView(
|
|||||||
videoUri: String,
|
videoUri: String,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
@@ -151,6 +160,7 @@ fun VideoView(
|
|||||||
defaultToStart,
|
defaultToStart,
|
||||||
title,
|
title,
|
||||||
thumb,
|
thumb,
|
||||||
|
waveform,
|
||||||
artworkUri,
|
artworkUri,
|
||||||
authorName,
|
authorName,
|
||||||
nostrUriCallback,
|
nostrUriCallback,
|
||||||
@@ -167,6 +177,7 @@ fun VideoViewInner(
|
|||||||
defaultToStart: Boolean = false,
|
defaultToStart: Boolean = false,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
@@ -220,7 +231,7 @@ fun VideoViewInner(
|
|||||||
defaultToStart = defaultToStart,
|
defaultToStart = defaultToStart,
|
||||||
nostrUriCallback = nostrUriCallback
|
nostrUriCallback = nostrUriCallback
|
||||||
) { controller, keepPlaying ->
|
) { controller, keepPlaying ->
|
||||||
RenderVideoPlayer(controller, thumb, keepPlaying, automaticallyStartPlayback, activeOnScreen, onDialog)
|
RenderVideoPlayer(controller, thumb, waveform, keepPlaying, automaticallyStartPlayback, activeOnScreen, onDialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +483,7 @@ data class VideoThumb(
|
|||||||
private fun RenderVideoPlayer(
|
private fun RenderVideoPlayer(
|
||||||
controller: MediaController,
|
controller: MediaController,
|
||||||
thumbData: VideoThumb?,
|
thumbData: VideoThumb?,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
keepPlaying: MutableState<Boolean>,
|
keepPlaying: MutableState<Boolean>,
|
||||||
automaticallyStartPlayback: MutableState<Boolean>,
|
automaticallyStartPlayback: MutableState<Boolean>,
|
||||||
activeOnScreen: MutableState<Boolean>,
|
activeOnScreen: MutableState<Boolean>,
|
||||||
@@ -489,7 +501,7 @@ private fun RenderVideoPlayer(
|
|||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.defaultMinSize(minHeight = 70.dp)
|
.defaultMinSize(minHeight = 100.dp)
|
||||||
.align(Alignment.Center),
|
.align(Alignment.Center),
|
||||||
factory = {
|
factory = {
|
||||||
PlayerView(context).apply {
|
PlayerView(context).apply {
|
||||||
@@ -518,6 +530,10 @@ private fun RenderVideoPlayer(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
waveform?.let {
|
||||||
|
Waveform(it, controller, Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
|
||||||
val startingMuteState = remember(controller) {
|
val startingMuteState = remember(controller) {
|
||||||
controller.volume < 0.001
|
controller.volume < 0.001
|
||||||
}
|
}
|
||||||
@@ -555,6 +571,69 @@ private fun RenderVideoPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pollCurrentDuration(controller: MediaController) = flow {
|
||||||
|
while (controller.currentPosition <= controller.duration) {
|
||||||
|
emit(controller.currentPosition / controller.duration.toFloat())
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}.conflate()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Waveform(
|
||||||
|
waveform: ImmutableList<Int>,
|
||||||
|
controller: MediaController,
|
||||||
|
align: Modifier
|
||||||
|
) {
|
||||||
|
val waveformProgress = remember { mutableStateOf(0F) }
|
||||||
|
|
||||||
|
DrawWaveform(waveform, waveformProgress, align)
|
||||||
|
|
||||||
|
val restartFlow = remember {
|
||||||
|
mutableStateOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps the screen on while playing and viewing videos.
|
||||||
|
DisposableEffect(key1 = controller) {
|
||||||
|
val listener = object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
// doesn't consider the mutex because the screen can turn off if the video
|
||||||
|
// being played in the mutex is not visible.
|
||||||
|
if (isPlaying) {
|
||||||
|
restartFlow.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.addListener(listener)
|
||||||
|
onDispose {
|
||||||
|
controller.removeListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = restartFlow.value) {
|
||||||
|
pollCurrentDuration(controller).collect() { value ->
|
||||||
|
waveformProgress.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawWaveform(waveform: ImmutableList<Int>, waveformProgress: MutableState<Float>, align: Modifier) {
|
||||||
|
AudioWaveformReadOnly(
|
||||||
|
modifier = align,
|
||||||
|
amplitudes = waveform,
|
||||||
|
progress = waveformProgress.value,
|
||||||
|
progressBrush = Brush.infiniteLinearGradient(
|
||||||
|
colors = listOf(Color(0xff2598cf), Color(0xff652d80)),
|
||||||
|
animation = tween(durationMillis = 6000, easing = LinearEasing),
|
||||||
|
width = 128F
|
||||||
|
),
|
||||||
|
onProgressChange = {
|
||||||
|
waveformProgress.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ControlWhenPlayerIsActive(
|
fun ControlWhenPlayerIsActive(
|
||||||
controller: Player,
|
controller: Player,
|
||||||
|
@@ -3386,6 +3386,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
|||||||
@Composable
|
@Composable
|
||||||
fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
val media = remember { noteEvent.stream() ?: noteEvent.download() }
|
val media = remember { noteEvent.stream() ?: noteEvent.download() }
|
||||||
|
val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } }
|
||||||
val subject = remember { noteEvent.subject()?.ifBlank { null } }
|
val subject = remember { noteEvent.subject()?.ifBlank { null } }
|
||||||
val content = remember { noteEvent.content().ifBlank { null } }
|
val content = remember { noteEvent.content().ifBlank { null } }
|
||||||
|
|
||||||
@@ -3397,6 +3398,7 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
|
|||||||
) {
|
) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = media,
|
videoUri = media,
|
||||||
|
waveform = waveform,
|
||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
|
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
@@ -383,6 +384,8 @@ fun NoteMaster(
|
|||||||
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is AudioTrackEvent) {
|
} else if (noteEvent is AudioTrackEvent) {
|
||||||
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav)
|
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav)
|
||||||
|
} else if (noteEvent is AudioHeaderEvent) {
|
||||||
|
AudioHeader(noteEvent, baseNote, accountViewModel, nav)
|
||||||
} else if (noteEvent is CommunityPostApprovalEvent) {
|
} else if (noteEvent is CommunityPostApprovalEvent) {
|
||||||
RenderPostApproval(
|
RenderPostApproval(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.quartz.events
|
package com.vitorpamplona.quartz.events
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
@@ -18,7 +19,9 @@ class AudioHeaderEvent(
|
|||||||
|
|
||||||
fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1)
|
fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1)
|
||||||
fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1)
|
fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1)
|
||||||
fun wavefrom() = tags.firstOrNull { it.size > 1 && it[0] == WAVEFORM }?.get(1)
|
fun wavefrom() = tags.firstOrNull { it.size > 1 && it[0] == WAVEFORM }?.get(1)?.let {
|
||||||
|
mapper.readValue<List<Int>>(it)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 1808
|
const val kind = 1808
|
||||||
|
Reference in New Issue
Block a user