mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: display file attachments in Feed and Detail screens
- Extend FeedPost with fileUrl and fileName fields - Parse mobiledoc file cards in FeedViewModel.extractFileCardFromMobiledoc() - Map LocalPost file fields to FeedPost in toFeedPost() - Create FileAttachmentCard composable with file type icon colors and tap-to-download - Integrate file card into PostCardContent (FeedScreen) and DetailScreen
This commit is contained in:
parent
74dac1db6f
commit
c55881e7a8
4 changed files with 132 additions and 3 deletions
|
|
@ -143,7 +143,9 @@ data class FeedPost(
|
|||
val createdAt: String?,
|
||||
val updatedAt: String?,
|
||||
val isLocal: Boolean = false,
|
||||
val queueStatus: QueueStatus = QueueStatus.NONE
|
||||
val queueStatus: QueueStatus = QueueStatus.NONE,
|
||||
val fileUrl: String? = null,
|
||||
val fileName: String? = null
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import com.swoosh.microblog.data.model.PostStats
|
|||
import com.swoosh.microblog.data.model.QueueStatus
|
||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
||||
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||
import com.swoosh.microblog.ui.feed.FileAttachmentCard
|
||||
import com.swoosh.microblog.ui.feed.FullScreenGallery
|
||||
import com.swoosh.microblog.ui.feed.StatusBadge
|
||||
import com.swoosh.microblog.ui.feed.formatRelativeTime
|
||||
|
|
@ -348,6 +349,22 @@ fun DetailScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// File attachment
|
||||
if (post.fileUrl != null) {
|
||||
AnimatedVisibility(
|
||||
visible = sectionVisible[4].value,
|
||||
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FileAttachmentCard(
|
||||
fileUrl = post.fileUrl,
|
||||
fileName = post.fileName ?: "File"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section 5 — PostStatsSection
|
||||
AnimatedVisibility(
|
||||
visible = sectionVisible[5].value,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import androidx.compose.ui.text.withStyle
|
|||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -1591,6 +1592,15 @@ fun PostCardContent(
|
|||
}
|
||||
}
|
||||
|
||||
// File attachment
|
||||
if (post.fileUrl != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
FileAttachmentCard(
|
||||
fileUrl = post.fileUrl,
|
||||
fileName = post.fileName ?: "File"
|
||||
)
|
||||
}
|
||||
|
||||
// Tags display
|
||||
if (post.tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
|
@ -2176,3 +2186,73 @@ fun StatusBadge(post: FeedPost) {
|
|||
border = null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an appropriate icon tint color based on file extension.
|
||||
*/
|
||||
@Composable
|
||||
private fun fileTypeColorFromName(fileName: String): Color {
|
||||
val lower = fileName.lowercase()
|
||||
return when {
|
||||
lower.endsWith(".pdf") -> Color(0xFFD32F2F) // red
|
||||
lower.endsWith(".doc") || lower.endsWith(".docx") -> Color(0xFF1565C0) // blue
|
||||
lower.endsWith(".txt") || lower.endsWith(".csv") -> Color(0xFF757575) // gray
|
||||
lower.endsWith(".xls") || lower.endsWith(".xlsx") -> Color(0xFF2E7D32) // green
|
||||
lower.endsWith(".ppt") || lower.endsWith(".pptx") -> Color(0xFFE65100) // orange
|
||||
lower.endsWith(".zip") || lower.endsWith(".rar") || lower.endsWith(".gz") -> Color(0xFF6A1B9A) // purple
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Card displaying a file attachment in Feed/Detail screens.
|
||||
* Tapping opens the file URL in an external viewer.
|
||||
*/
|
||||
@Composable
|
||||
fun FileAttachmentCard(
|
||||
fileUrl: String,
|
||||
fileName: String
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val iconTint = fileTypeColorFromName(fileName)
|
||||
|
||||
OutlinedCard(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(fileUrl))
|
||||
context.startActivity(intent)
|
||||
} catch (_: Exception) {
|
||||
// No handler available for this file type
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.InsertDriveFile,
|
||||
contentDescription = "File attachment",
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = fileName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "Tap to download",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -535,6 +535,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
allImages.add(url)
|
||||
}
|
||||
}
|
||||
val fileData = extractFileCardFromMobiledoc(mobiledoc)
|
||||
return FeedPost(
|
||||
ghostId = id,
|
||||
slug = slug,
|
||||
|
|
@ -555,7 +556,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
publishedAt = published_at,
|
||||
createdAt = created_at,
|
||||
updatedAt = updated_at,
|
||||
isLocal = false
|
||||
isLocal = false,
|
||||
fileUrl = fileData?.first,
|
||||
fileName = fileData?.second
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -580,6 +583,31 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts file card data from Ghost mobiledoc JSON.
|
||||
* File cards have the format: ["file", {"src": "url", "fileName": "name"}]
|
||||
* Returns a Pair of (fileUrl, fileName) or null if no file card.
|
||||
*/
|
||||
private fun extractFileCardFromMobiledoc(mobiledoc: String?): Pair<String, String>? {
|
||||
if (mobiledoc == null) return null
|
||||
return try {
|
||||
val json = com.google.gson.JsonParser.parseString(mobiledoc).asJsonObject
|
||||
val cards = json.getAsJsonArray("cards") ?: return null
|
||||
for (card in cards) {
|
||||
val cardArray = card.asJsonArray
|
||||
if (cardArray.size() >= 2 && cardArray[0].asString == "file") {
|
||||
val cardData = cardArray[1].asJsonObject
|
||||
val src = cardData.get("src")?.asString ?: continue
|
||||
val name = cardData.get("fileName")?.asString ?: "file"
|
||||
return Pair(src, name)
|
||||
}
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun LocalPost.toFeedPost(): FeedPost {
|
||||
val tagNames: List<String> = try {
|
||||
Gson().fromJson(tags, object : TypeToken<List<String>>() {}.type) ?: emptyList()
|
||||
|
|
@ -615,7 +643,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
createdAt = null,
|
||||
updatedAt = null,
|
||||
isLocal = true,
|
||||
queueStatus = queueStatus
|
||||
queueStatus = queueStatus,
|
||||
fileUrl = uploadedFileUrl ?: fileUri,
|
||||
fileName = fileName
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue