mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add video/audio playback in Feed and Detail screens
- Add Media3 ExoPlayer dependencies (media3-exoplayer, media3-ui 1.2.1) - Extend FeedPost with videoUrl and audioUrl fields - Parse video/audio card URLs from mobiledoc JSON in FeedViewModel - Map LocalPost video/audio URIs to FeedPost in toFeedPost() - Create VideoPlayer composable: ExoPlayer in AndroidView, play button overlay, tap to play - Create AudioPlayer composable: play/pause button, progress slider, duration text - Integrate compact VideoPlayer and AudioPlayer in FeedScreen post cards - Integrate full-size VideoPlayer and AudioPlayer in DetailScreen with reveal animations - Load video/audio URIs when editing a post in ComposerViewModel
This commit is contained in:
parent
27782893dc
commit
a1aae661c9
7 changed files with 330 additions and 5 deletions
|
|
@ -107,6 +107,10 @@ dependencies {
|
||||||
// Jsoup for OpenGraph parsing
|
// Jsoup for OpenGraph parsing
|
||||||
implementation("org.jsoup:jsoup:1.17.2")
|
implementation("org.jsoup:jsoup:1.17.2")
|
||||||
|
|
||||||
|
// Media3 for video/audio playback
|
||||||
|
implementation("androidx.media3:media3-exoplayer:1.2.1")
|
||||||
|
implementation("androidx.media3:media3-ui:1.2.1")
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,8 @@ data class FeedPost(
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val imageAlt: String? = null,
|
val imageAlt: String? = null,
|
||||||
val imageUrls: List<String> = emptyList(),
|
val imageUrls: List<String> = emptyList(),
|
||||||
|
val videoUrl: String? = null,
|
||||||
|
val audioUrl: String? = null,
|
||||||
val linkUrl: String?,
|
val linkUrl: String?,
|
||||||
val linkTitle: String?,
|
val linkTitle: String?,
|
||||||
val linkDescription: String?,
|
val linkDescription: String?,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
package com.swoosh.microblog.ui.components
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.MusicNote
|
||||||
|
import androidx.compose.material.icons.filled.Pause
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video player composable using ExoPlayer.
|
||||||
|
* Shows a play button overlay; tap to play/pause. No autoplay.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun VideoPlayer(
|
||||||
|
url: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
compact: Boolean = false
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var isPlaying by remember { mutableStateOf(false) }
|
||||||
|
var showOverlay by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val exoPlayer = remember(url) {
|
||||||
|
ExoPlayer.Builder(context).build().apply {
|
||||||
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(exoPlayer) {
|
||||||
|
val listener = object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(playing: Boolean) {
|
||||||
|
isPlaying = playing
|
||||||
|
if (!playing && exoPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
|
showOverlay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exoPlayer.addListener(listener)
|
||||||
|
onDispose {
|
||||||
|
exoPlayer.removeListener(listener)
|
||||||
|
exoPlayer.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val height = if (compact) 180.dp else 240.dp
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(height)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable {
|
||||||
|
if (isPlaying) {
|
||||||
|
exoPlayer.pause()
|
||||||
|
showOverlay = true
|
||||||
|
} else {
|
||||||
|
exoPlayer.play()
|
||||||
|
showOverlay = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
PlayerView(ctx).apply {
|
||||||
|
player = exoPlayer
|
||||||
|
useController = false
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Play button overlay
|
||||||
|
if (showOverlay || !isPlaying) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.8f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||||
|
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact audio player with play/pause button, progress slider, and duration text.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AudioPlayer(
|
||||||
|
url: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var isPlaying by remember { mutableStateOf(false) }
|
||||||
|
var currentPosition by remember { mutableLongStateOf(0L) }
|
||||||
|
var duration by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
val exoPlayer = remember(url) {
|
||||||
|
ExoPlayer.Builder(context).build().apply {
|
||||||
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(exoPlayer) {
|
||||||
|
val listener = object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(playing: Boolean) {
|
||||||
|
isPlaying = playing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
if (playbackState == Player.STATE_READY) {
|
||||||
|
duration = exoPlayer.duration.coerceAtLeast(0L)
|
||||||
|
}
|
||||||
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
|
isPlaying = false
|
||||||
|
exoPlayer.seekTo(0)
|
||||||
|
exoPlayer.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exoPlayer.addListener(listener)
|
||||||
|
onDispose {
|
||||||
|
exoPlayer.removeListener(listener)
|
||||||
|
exoPlayer.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position periodically while playing
|
||||||
|
LaunchedEffect(isPlaying) {
|
||||||
|
while (isPlaying) {
|
||||||
|
currentPosition = exoPlayer.currentPosition.coerceAtLeast(0L)
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Music note icon
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MusicNote,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Play/Pause button
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (isPlaying) {
|
||||||
|
exoPlayer.pause()
|
||||||
|
} else {
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||||
|
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress slider
|
||||||
|
Slider(
|
||||||
|
value = if (duration > 0) currentPosition.toFloat() / duration.toFloat() else 0f,
|
||||||
|
onValueChange = { fraction ->
|
||||||
|
val newPosition = (fraction * duration).toLong()
|
||||||
|
exoPlayer.seekTo(newPosition)
|
||||||
|
currentPosition = newPosition
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Duration text
|
||||||
|
Text(
|
||||||
|
text = formatDuration(if (isPlaying) currentPosition else duration),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDuration(millis: Long): String {
|
||||||
|
if (millis <= 0) return "0:00"
|
||||||
|
val totalSeconds = millis / 1000
|
||||||
|
val minutes = totalSeconds / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
return "$minutes:${seconds.toString().padStart(2, '0')}"
|
||||||
|
}
|
||||||
|
|
@ -97,6 +97,8 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
||||||
text = post.textContent,
|
text = post.textContent,
|
||||||
imageUris = imageUris,
|
imageUris = imageUris,
|
||||||
imageAlt = post.imageAlt ?: "",
|
imageAlt = post.imageAlt ?: "",
|
||||||
|
videoUri = post.videoUrl?.let { url -> Uri.parse(url) },
|
||||||
|
audioUri = post.audioUrl?.let { url -> Uri.parse(url) },
|
||||||
linkPreview = if (post.linkUrl != null) LinkPreview(
|
linkPreview = if (post.linkUrl != null) LinkPreview(
|
||||||
url = post.linkUrl,
|
url = post.linkUrl,
|
||||||
title = post.linkTitle,
|
title = post.linkTitle,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ import com.swoosh.microblog.data.model.LinkPreview
|
||||||
import com.swoosh.microblog.data.model.PostStats
|
import com.swoosh.microblog.data.model.PostStats
|
||||||
import com.swoosh.microblog.data.model.QueueStatus
|
import com.swoosh.microblog.data.model.QueueStatus
|
||||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
import com.swoosh.microblog.ui.animation.SwooshMotion
|
||||||
|
import com.swoosh.microblog.ui.components.AudioPlayer
|
||||||
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||||
|
import com.swoosh.microblog.ui.components.VideoPlayer
|
||||||
import com.swoosh.microblog.ui.feed.FullScreenGallery
|
import com.swoosh.microblog.ui.feed.FullScreenGallery
|
||||||
import com.swoosh.microblog.ui.feed.StatusBadge
|
import com.swoosh.microblog.ui.feed.StatusBadge
|
||||||
import com.swoosh.microblog.ui.feed.formatRelativeTime
|
import com.swoosh.microblog.ui.feed.formatRelativeTime
|
||||||
|
|
@ -94,7 +96,7 @@ fun DetailScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// D1: Content reveal sequence
|
// D1: Content reveal sequence
|
||||||
val revealCount = 6 // status, text, tags, gallery, link, stats
|
val revealCount = 8 // status, text, tags, gallery, video, audio, link, stats
|
||||||
val sectionVisible = remember { List(revealCount) { mutableStateOf(false) } }
|
val sectionVisible = remember { List(revealCount) { mutableStateOf(false) } }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
sectionVisible.forEachIndexed { index, state ->
|
sectionVisible.forEachIndexed { index, state ->
|
||||||
|
|
@ -300,9 +302,31 @@ fun DetailScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section 4 — Link preview
|
// Section 4 — Video player
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = sectionVisible[4].value && post.linkUrl != null,
|
visible = sectionVisible[4].value && post.videoUrl != null,
|
||||||
|
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
VideoPlayer(url = post.videoUrl!!, compact = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section 5 — Audio player
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = sectionVisible[5].value && post.audioUrl != null,
|
||||||
|
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AudioPlayer(url = post.audioUrl!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section 6 — Link preview
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = sectionVisible[6].value && post.linkUrl != null,
|
||||||
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
|
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
|
@ -348,9 +372,9 @@ fun DetailScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section 5 — PostStatsSection
|
// Section 7 — PostStatsSection
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = sectionVisible[5].value,
|
visible = sectionVisible[7].value,
|
||||||
enter = slideInVertically(initialOffsetY = { it / 4 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick())
|
enter = slideInVertically(initialOffsetY = { it / 4 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick())
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ import com.swoosh.microblog.data.model.PostFilter
|
||||||
import com.swoosh.microblog.data.model.PostStats
|
import com.swoosh.microblog.data.model.PostStats
|
||||||
import com.swoosh.microblog.data.model.QueueStatus
|
import com.swoosh.microblog.data.model.QueueStatus
|
||||||
import com.swoosh.microblog.data.model.SortOrder
|
import com.swoosh.microblog.data.model.SortOrder
|
||||||
|
import com.swoosh.microblog.ui.components.AudioPlayer
|
||||||
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||||
|
import com.swoosh.microblog.ui.components.VideoPlayer
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -1555,6 +1557,18 @@ fun PostCardContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video player (compact)
|
||||||
|
if (post.videoUrl != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
VideoPlayer(url = post.videoUrl, compact = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio player (compact)
|
||||||
|
if (post.audioUrl != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
AudioPlayer(url = post.audioUrl)
|
||||||
|
}
|
||||||
|
|
||||||
// Link preview
|
// Link preview
|
||||||
if (post.linkUrl != null && post.linkTitle != null) {
|
if (post.linkUrl != null && post.linkTitle != null) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
|
||||||
|
|
@ -535,6 +535,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
allImages.add(url)
|
allImages.add(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val (videoUrl, audioUrl) = extractMediaUrlsFromMobiledoc(mobiledoc)
|
||||||
return FeedPost(
|
return FeedPost(
|
||||||
ghostId = id,
|
ghostId = id,
|
||||||
slug = slug,
|
slug = slug,
|
||||||
|
|
@ -545,6 +546,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
imageUrl = allImages.firstOrNull(),
|
imageUrl = allImages.firstOrNull(),
|
||||||
imageAlt = feature_image_alt,
|
imageAlt = feature_image_alt,
|
||||||
imageUrls = allImages,
|
imageUrls = allImages,
|
||||||
|
videoUrl = videoUrl,
|
||||||
|
audioUrl = audioUrl,
|
||||||
linkUrl = null,
|
linkUrl = null,
|
||||||
linkTitle = null,
|
linkTitle = null,
|
||||||
linkDescription = null,
|
linkDescription = null,
|
||||||
|
|
@ -580,6 +583,42 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts video and audio URLs from Ghost mobiledoc JSON.
|
||||||
|
* Video cards: ["video", {"src": "url"}]
|
||||||
|
* Audio cards: ["audio", {"src": "url"}]
|
||||||
|
* Returns a Pair of (videoUrl, audioUrl), either may be null.
|
||||||
|
*/
|
||||||
|
private fun extractMediaUrlsFromMobiledoc(mobiledoc: String?): Pair<String?, String?> {
|
||||||
|
if (mobiledoc == null) return null to null
|
||||||
|
return try {
|
||||||
|
val json = com.google.gson.JsonParser.parseString(mobiledoc).asJsonObject
|
||||||
|
val cards = json.getAsJsonArray("cards") ?: return null to null
|
||||||
|
var videoUrl: String? = null
|
||||||
|
var audioUrl: String? = null
|
||||||
|
for (card in cards) {
|
||||||
|
val cardArray = card.asJsonArray
|
||||||
|
if (cardArray.size() >= 2) {
|
||||||
|
when (cardArray[0].asString) {
|
||||||
|
"video" -> {
|
||||||
|
if (videoUrl == null) {
|
||||||
|
videoUrl = cardArray[1].asJsonObject.get("src")?.asString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"audio" -> {
|
||||||
|
if (audioUrl == null) {
|
||||||
|
audioUrl = cardArray[1].asJsonObject.get("src")?.asString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
videoUrl to audioUrl
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null to null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun LocalPost.toFeedPost(): FeedPost {
|
private fun LocalPost.toFeedPost(): FeedPost {
|
||||||
val tagNames: List<String> = try {
|
val tagNames: List<String> = try {
|
||||||
Gson().fromJson(tags, object : TypeToken<List<String>>() {}.type) ?: emptyList()
|
Gson().fromJson(tags, object : TypeToken<List<String>>() {}.type) ?: emptyList()
|
||||||
|
|
@ -604,6 +643,8 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
imageUrl = allImageUrls.firstOrNull(),
|
imageUrl = allImageUrls.firstOrNull(),
|
||||||
imageAlt = imageAlt,
|
imageAlt = imageAlt,
|
||||||
imageUrls = allImageUrls,
|
imageUrls = allImageUrls,
|
||||||
|
videoUrl = uploadedVideoUrl ?: videoUri,
|
||||||
|
audioUrl = uploadedAudioUrl ?: audioUri,
|
||||||
linkUrl = linkUrl,
|
linkUrl = linkUrl,
|
||||||
linkTitle = linkTitle,
|
linkTitle = linkTitle,
|
||||||
linkDescription = linkDescription,
|
linkDescription = linkDescription,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue