mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
refactor: simplify code after /simplify review
- Parallelize StatsViewModel fetches (posts, members, tags via async/await) - Collapse MobiledocBuilder overloads into single function with defaults - Extract shared FileTypeColor composable from duplicated color mappings - Remove redundant state in FeedViewModel and FeedScreen - Unify formatFileSize usage, remove inline duplication - Fix minor issues in MediaPlayers, PostUploadWorker, Pages, Settings
This commit is contained in:
parent
29927a7638
commit
da8a90470d
13 changed files with 305 additions and 376 deletions
|
|
@ -9,81 +9,7 @@ import com.swoosh.microblog.data.model.LinkPreview
|
||||||
object MobiledocBuilder {
|
object MobiledocBuilder {
|
||||||
|
|
||||||
fun build(text: String, linkPreview: LinkPreview?): String {
|
fun build(text: String, linkPreview: LinkPreview?): String {
|
||||||
return build(text, linkPreview?.url, linkPreview?.title, linkPreview?.description)
|
return build(text, linkUrl = linkPreview?.url, linkTitle = linkPreview?.title, linkDescription = linkPreview?.description)
|
||||||
}
|
|
||||||
|
|
||||||
fun build(
|
|
||||||
text: String,
|
|
||||||
linkUrl: String?,
|
|
||||||
linkTitle: String?,
|
|
||||||
linkDescription: String?
|
|
||||||
): String {
|
|
||||||
return build(text, emptyList(), linkUrl, linkTitle, linkDescription, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build with a single image URL and optional alt text (HEAD's 6-param overload).
|
|
||||||
*/
|
|
||||||
fun build(
|
|
||||||
text: String,
|
|
||||||
linkUrl: String?,
|
|
||||||
linkTitle: String?,
|
|
||||||
linkDescription: String?,
|
|
||||||
imageUrl: String?,
|
|
||||||
imageAlt: String?
|
|
||||||
): String {
|
|
||||||
val imageUrls = if (imageUrl != null) listOf(imageUrl) else emptyList()
|
|
||||||
return build(text, imageUrls, linkUrl, linkTitle, linkDescription, imageAlt)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build with multiple image URLs but no alt text (multi-image branch's 5-param overload).
|
|
||||||
*/
|
|
||||||
fun build(
|
|
||||||
text: String,
|
|
||||||
imageUrls: List<String>,
|
|
||||||
linkUrl: String?,
|
|
||||||
linkTitle: String?,
|
|
||||||
linkDescription: String?
|
|
||||||
): String {
|
|
||||||
return build(text, imageUrls, linkUrl, linkTitle, linkDescription, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build with multiple images, alt text, and optional video/audio.
|
|
||||||
* Delegates to the full implementation.
|
|
||||||
*/
|
|
||||||
fun build(
|
|
||||||
text: String,
|
|
||||||
imageUrls: List<String>,
|
|
||||||
linkUrl: String?,
|
|
||||||
linkTitle: String?,
|
|
||||||
linkDescription: String?,
|
|
||||||
imageAlt: String?
|
|
||||||
): String {
|
|
||||||
return build(text, imageUrls, linkUrl, linkTitle, linkDescription, imageAlt, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build with images, video, audio (no file).
|
|
||||||
* Delegates to the full implementation.
|
|
||||||
*/
|
|
||||||
fun build(
|
|
||||||
text: String,
|
|
||||||
imageUrls: List<String>,
|
|
||||||
linkUrl: String?,
|
|
||||||
linkTitle: String?,
|
|
||||||
linkDescription: String?,
|
|
||||||
imageAlt: String?,
|
|
||||||
videoUrl: String?,
|
|
||||||
audioUrl: String?
|
|
||||||
): String {
|
|
||||||
return build(
|
|
||||||
text = text, imageUrls = imageUrls,
|
|
||||||
linkUrl = linkUrl, linkTitle = linkTitle, linkDescription = linkDescription,
|
|
||||||
imageAlt = imageAlt, videoUrl = videoUrl, audioUrl = audioUrl,
|
|
||||||
fileUrl = null, fileName = null, fileSize = 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,11 +20,11 @@ object MobiledocBuilder {
|
||||||
*/
|
*/
|
||||||
fun build(
|
fun build(
|
||||||
text: String,
|
text: String,
|
||||||
imageUrls: List<String>,
|
imageUrls: List<String> = emptyList(),
|
||||||
linkUrl: String?,
|
linkUrl: String? = null,
|
||||||
linkTitle: String?,
|
linkTitle: String? = null,
|
||||||
linkDescription: String?,
|
linkDescription: String? = null,
|
||||||
imageAlt: String?,
|
imageAlt: String? = null,
|
||||||
videoUrl: String? = null,
|
videoUrl: String? = null,
|
||||||
audioUrl: String? = null,
|
audioUrl: String? = null,
|
||||||
fileUrl: String? = null,
|
fileUrl: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,10 @@ object UrlNormalizer {
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips the URL scheme (http/https) and trailing slash for display purposes.
|
||||||
|
* e.g., "https://example.com/" -> "example.com"
|
||||||
|
*/
|
||||||
|
fun String.toDisplayUrl(): String =
|
||||||
|
removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.swoosh.microblog.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an appropriate icon tint color for a file based on MIME type or file name.
|
||||||
|
* Accepts either a MIME type string, a file name, or both (MIME type takes priority).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun fileTypeColor(mimeType: String? = null, fileName: String? = null): Color {
|
||||||
|
// Try MIME type first
|
||||||
|
if (mimeType != null) {
|
||||||
|
val color = colorForMimeType(mimeType)
|
||||||
|
if (color != null) return color
|
||||||
|
}
|
||||||
|
// Fall back to file extension
|
||||||
|
if (fileName != null) {
|
||||||
|
val color = colorForFileName(fileName)
|
||||||
|
if (color != null) return color
|
||||||
|
}
|
||||||
|
return MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorForMimeType(mimeType: String): Color? = when {
|
||||||
|
mimeType.contains("pdf") -> Color(0xFFD32F2F)
|
||||||
|
mimeType.contains("word") || mimeType.contains("doc") -> Color(0xFF1565C0)
|
||||||
|
mimeType.contains("text") -> Color(0xFF757575)
|
||||||
|
mimeType.contains("spreadsheet") || mimeType.contains("excel") -> Color(0xFF2E7D32)
|
||||||
|
mimeType.contains("presentation") || mimeType.contains("powerpoint") -> Color(0xFFE65100)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorForFileName(fileName: String): Color? {
|
||||||
|
val lower = fileName.lowercase()
|
||||||
|
return when {
|
||||||
|
lower.endsWith(".pdf") -> Color(0xFFD32F2F)
|
||||||
|
lower.endsWith(".doc") || lower.endsWith(".docx") -> Color(0xFF1565C0)
|
||||||
|
lower.endsWith(".txt") || lower.endsWith(".csv") -> Color(0xFF757575)
|
||||||
|
lower.endsWith(".xls") || lower.endsWith(".xlsx") -> Color(0xFF2E7D32)
|
||||||
|
lower.endsWith(".ppt") || lower.endsWith(".pptx") -> Color(0xFFE65100)
|
||||||
|
lower.endsWith(".zip") || lower.endsWith(".rar") || lower.endsWith(".gz") -> Color(0xFF6A1B9A)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,8 @@ import androidx.media3.ui.PlayerView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Video player composable using ExoPlayer.
|
* Video player composable using ExoPlayer.
|
||||||
* Shows a play button overlay; tap to play/pause. No autoplay.
|
* Shows a play button overlay; tap to play/pause.
|
||||||
|
* Media is only prepared (buffered) on first play tap to avoid unnecessary network usage.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayer(
|
fun VideoPlayer(
|
||||||
|
|
@ -35,11 +36,11 @@ fun VideoPlayer(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var isPlaying by remember { mutableStateOf(false) }
|
var isPlaying by remember { mutableStateOf(false) }
|
||||||
var showOverlay by remember { mutableStateOf(true) }
|
var showOverlay by remember { mutableStateOf(true) }
|
||||||
|
var hasPrepared by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val exoPlayer = remember(url) {
|
val exoPlayer = remember(url) {
|
||||||
ExoPlayer.Builder(context).build().apply {
|
ExoPlayer.Builder(context).build().apply {
|
||||||
setMediaItem(MediaItem.fromUri(url))
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
prepare()
|
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +73,10 @@ fun VideoPlayer(
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
showOverlay = true
|
showOverlay = true
|
||||||
} else {
|
} else {
|
||||||
|
if (!hasPrepared) {
|
||||||
|
exoPlayer.prepare()
|
||||||
|
hasPrepared = true
|
||||||
|
}
|
||||||
exoPlayer.play()
|
exoPlayer.play()
|
||||||
showOverlay = false
|
showOverlay = false
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +119,7 @@ fun VideoPlayer(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compact audio player with play/pause button, progress slider, and duration text.
|
* Compact audio player with play/pause button, progress slider, and duration text.
|
||||||
|
* Media is only prepared (buffered) on first play tap to avoid unnecessary network usage.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioPlayer(
|
fun AudioPlayer(
|
||||||
|
|
@ -124,11 +130,11 @@ fun AudioPlayer(
|
||||||
var isPlaying by remember { mutableStateOf(false) }
|
var isPlaying by remember { mutableStateOf(false) }
|
||||||
var currentPosition by remember { mutableLongStateOf(0L) }
|
var currentPosition by remember { mutableLongStateOf(0L) }
|
||||||
var duration by remember { mutableLongStateOf(0L) }
|
var duration by remember { mutableLongStateOf(0L) }
|
||||||
|
var hasPrepared by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val exoPlayer = remember(url) {
|
val exoPlayer = remember(url) {
|
||||||
ExoPlayer.Builder(context).build().apply {
|
ExoPlayer.Builder(context).build().apply {
|
||||||
setMediaItem(MediaItem.fromUri(url))
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
prepare()
|
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +196,10 @@ fun AudioPlayer(
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
} else {
|
} else {
|
||||||
|
if (!hasPrepared) {
|
||||||
|
exoPlayer.prepare()
|
||||||
|
hasPrepared = true
|
||||||
|
}
|
||||||
exoPlayer.play()
|
exoPlayer.play()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1429,12 +1429,7 @@ fun MediaPreviewCard(
|
||||||
context.contentResolver.query(mediaUri, null, null, null, null)?.use { cursor ->
|
context.contentResolver.query(mediaUri, null, null, null, null)?.use { cursor ->
|
||||||
val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE)
|
val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE)
|
||||||
if (cursor.moveToFirst() && sizeIndex >= 0) {
|
if (cursor.moveToFirst() && sizeIndex >= 0) {
|
||||||
val bytes = cursor.getLong(sizeIndex)
|
formatFileSize(cursor.getLong(sizeIndex))
|
||||||
when {
|
|
||||||
bytes < 1024 -> "$bytes B"
|
|
||||||
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
|
|
||||||
else -> "%.1f MB".format(bytes / (1024.0 * 1024.0))
|
|
||||||
}
|
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|
@ -1610,21 +1605,6 @@ fun TagsSection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an appropriate icon tint color based on file MIME type.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun fileTypeColor(mimeType: String?): Color {
|
|
||||||
return when {
|
|
||||||
mimeType == null -> MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
mimeType.contains("pdf") -> Color(0xFFD32F2F) // red for PDF
|
|
||||||
mimeType.contains("word") || mimeType.contains("doc") -> Color(0xFF1565C0) // blue for DOC
|
|
||||||
mimeType.contains("text") -> Color(0xFF757575) // gray for TXT
|
|
||||||
mimeType.contains("spreadsheet") || mimeType.contains("excel") -> Color(0xFF2E7D32) // green for spreadsheets
|
|
||||||
mimeType.contains("presentation") || mimeType.contains("powerpoint") -> Color(0xFFE65100) // orange for presentations
|
|
||||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats file size in human-readable units.
|
* Formats file size in human-readable units.
|
||||||
|
|
@ -1647,7 +1627,7 @@ fun FileAttachmentComposerCard(
|
||||||
fileMimeType: String?,
|
fileMimeType: String?,
|
||||||
onRemove: () -> Unit
|
onRemove: () -> Unit
|
||||||
) {
|
) {
|
||||||
val iconTint = fileTypeColor(fileMimeType)
|
val iconTint = com.swoosh.microblog.ui.components.fileTypeColor(mimeType = fileMimeType)
|
||||||
|
|
||||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.swoosh.microblog.data.CredentialsManager
|
import com.swoosh.microblog.data.CredentialsManager
|
||||||
|
import com.swoosh.microblog.data.toDisplayUrl
|
||||||
import com.swoosh.microblog.data.ShareUtils
|
import com.swoosh.microblog.data.ShareUtils
|
||||||
import com.swoosh.microblog.data.SiteMetadataCache
|
import com.swoosh.microblog.data.SiteMetadataCache
|
||||||
import com.swoosh.microblog.data.model.FeedPost
|
import com.swoosh.microblog.data.model.FeedPost
|
||||||
|
|
@ -245,10 +246,7 @@ fun FeedScreen(
|
||||||
)
|
)
|
||||||
if (activeAccount != null) {
|
if (activeAccount != null) {
|
||||||
Text(
|
Text(
|
||||||
text = activeAccount!!.blogUrl
|
text = activeAccount!!.blogUrl.toDisplayUrl(),
|
||||||
.removePrefix("https://")
|
|
||||||
.removePrefix("http://")
|
|
||||||
.removeSuffix("/"),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
@ -264,7 +262,7 @@ fun FeedScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts.size > 1 || accounts.isNotEmpty()) {
|
if (accounts.size > 1) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.KeyboardArrowDown,
|
Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = "Switch account",
|
contentDescription = "Switch account",
|
||||||
|
|
@ -1178,10 +1176,7 @@ fun AccountListItem(
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
account.blogUrl
|
account.blogUrl.toDisplayUrl(),
|
||||||
.removePrefix("https://")
|
|
||||||
.removePrefix("http://")
|
|
||||||
.removeSuffix("/"),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -1443,11 +1438,6 @@ fun PostCardContent(
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var showContextMenu by remember { mutableStateOf(false) }
|
var showContextMenu by remember { mutableStateOf(false) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val displayText = if (expanded || post.textContent.length <= 280) {
|
|
||||||
post.textContent
|
|
||||||
} else {
|
|
||||||
post.textContent.take(280) + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
val isPublished = post.status == "published" && post.queueStatus == QueueStatus.NONE
|
val isPublished = post.status == "published" && post.queueStatus == QueueStatus.NONE
|
||||||
val hasShareableUrl = !post.slug.isNullOrBlank() || !post.url.isNullOrBlank()
|
val hasShareableUrl = !post.slug.isNullOrBlank() || !post.url.isNullOrBlank()
|
||||||
|
|
@ -1506,7 +1496,8 @@ fun PostCardContent(
|
||||||
// Content -- the star of the show
|
// Content -- the star of the show
|
||||||
if (highlightQuery != null && highlightQuery.isNotBlank()) {
|
if (highlightQuery != null && highlightQuery.isNotBlank()) {
|
||||||
HighlightedText(
|
HighlightedText(
|
||||||
text = displayText,
|
text = if (expanded || post.textContent.length <= 280) post.textContent
|
||||||
|
else post.textContent.take(280) + "...",
|
||||||
query = highlightQuery,
|
query = highlightQuery,
|
||||||
maxLines = if (expanded) Int.MAX_VALUE else 8
|
maxLines = if (expanded) Int.MAX_VALUE else 8
|
||||||
)
|
)
|
||||||
|
|
@ -2259,22 +2250,6 @@ fun StatusBadge(post: FeedPost) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Card displaying a file attachment in Feed/Detail screens.
|
||||||
|
|
@ -2286,7 +2261,7 @@ fun FileAttachmentCard(
|
||||||
fileName: String
|
fileName: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val iconTint = fileTypeColorFromName(fileName)
|
val iconTint = com.swoosh.microblog.ui.components.fileTypeColor(fileName = fileName)
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ data class SnackbarEvent(
|
||||||
|
|
||||||
class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
private val accountManager = AccountManager(application)
|
private val accountManager = AccountManager(application)
|
||||||
private var repository = PostRepository(application)
|
private var repository = PostRepository(application)
|
||||||
private var tagRepository = TagRepository(application)
|
private var tagRepository = TagRepository(application)
|
||||||
|
|
@ -523,20 +524,62 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
_uiState.update { it.copy(posts = sorted) }
|
_uiState.update { it.copy(posts = sorted) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed mobiledoc card data. Single-parse result for all card types.
|
||||||
|
*/
|
||||||
|
private data class MobiledocCards(
|
||||||
|
val imageUrls: List<String> = emptyList(),
|
||||||
|
val videoUrl: String? = null,
|
||||||
|
val audioUrl: String? = null,
|
||||||
|
val fileUrl: String? = null,
|
||||||
|
val fileName: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses mobiledoc JSON once and extracts all card data (images, video, audio, file).
|
||||||
|
*/
|
||||||
|
private fun parseMobiledocCards(mobiledoc: String?): MobiledocCards {
|
||||||
|
if (mobiledoc == null) return MobiledocCards()
|
||||||
|
return try {
|
||||||
|
val json = com.google.gson.JsonParser.parseString(mobiledoc).asJsonObject
|
||||||
|
val cards = json.getAsJsonArray("cards") ?: return MobiledocCards()
|
||||||
|
val imageUrls = mutableListOf<String>()
|
||||||
|
var videoUrl: String? = null
|
||||||
|
var audioUrl: String? = null
|
||||||
|
var fileUrl: String? = null
|
||||||
|
var fileName: String? = null
|
||||||
|
for (card in cards) {
|
||||||
|
val cardArray = card.asJsonArray
|
||||||
|
if (cardArray.size() < 2) continue
|
||||||
|
val cardData = cardArray[1].asJsonObject
|
||||||
|
when (cardArray[0].asString) {
|
||||||
|
"image" -> cardData.get("src")?.asString?.let { imageUrls.add(it) }
|
||||||
|
"video" -> if (videoUrl == null) videoUrl = cardData.get("src")?.asString
|
||||||
|
"audio" -> if (audioUrl == null) audioUrl = cardData.get("src")?.asString
|
||||||
|
"file" -> if (fileUrl == null) {
|
||||||
|
fileUrl = cardData.get("src")?.asString
|
||||||
|
fileName = cardData.get("fileName")?.asString ?: "file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MobiledocCards(imageUrls, videoUrl, audioUrl, fileUrl, fileName)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MobiledocCards()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun GhostPost.toFeedPost(): FeedPost {
|
private fun GhostPost.toFeedPost(): FeedPost {
|
||||||
val imageUrls = extractImageUrlsFromMobiledoc(mobiledoc)
|
val mobiledocCards = parseMobiledocCards(mobiledoc)
|
||||||
// Use feature_image as primary, then add mobiledoc images (avoiding duplicates)
|
// Use feature_image as primary, then add mobiledoc images (avoiding duplicates)
|
||||||
val allImages = mutableListOf<String>()
|
val allImages = mutableListOf<String>()
|
||||||
if (feature_image != null) {
|
if (feature_image != null) {
|
||||||
allImages.add(feature_image)
|
allImages.add(feature_image)
|
||||||
}
|
}
|
||||||
for (url in imageUrls) {
|
for (url in mobiledocCards.imageUrls) {
|
||||||
if (url !in allImages) {
|
if (url !in allImages) {
|
||||||
allImages.add(url)
|
allImages.add(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val fileData = extractFileCardFromMobiledoc(mobiledoc)
|
|
||||||
val (videoUrl, audioUrl) = extractMediaUrlsFromMobiledoc(mobiledoc)
|
|
||||||
val isEmailOnly = status == "sent" || email_only == true
|
val isEmailOnly = status == "sent" || email_only == true
|
||||||
return FeedPost(
|
return FeedPost(
|
||||||
ghostId = id,
|
ghostId = id,
|
||||||
|
|
@ -548,8 +591,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,
|
videoUrl = mobiledocCards.videoUrl,
|
||||||
audioUrl = audioUrl,
|
audioUrl = mobiledocCards.audioUrl,
|
||||||
linkUrl = null,
|
linkUrl = null,
|
||||||
linkTitle = null,
|
linkTitle = null,
|
||||||
linkDescription = null,
|
linkDescription = null,
|
||||||
|
|
@ -561,97 +604,15 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
createdAt = created_at,
|
createdAt = created_at,
|
||||||
updatedAt = updated_at,
|
updatedAt = updated_at,
|
||||||
isLocal = false,
|
isLocal = false,
|
||||||
fileUrl = fileData?.first,
|
fileUrl = mobiledocCards.fileUrl,
|
||||||
fileName = fileData?.second,
|
fileName = mobiledocCards.fileName,
|
||||||
emailOnly = isEmailOnly
|
emailOnly = isEmailOnly
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts image URLs from Ghost mobiledoc JSON.
|
|
||||||
* Image cards have the format: ["image", {"src": "url"}]
|
|
||||||
*/
|
|
||||||
private fun extractImageUrlsFromMobiledoc(mobiledoc: String?): List<String> {
|
|
||||||
if (mobiledoc == null) return emptyList()
|
|
||||||
return try {
|
|
||||||
val json = com.google.gson.JsonParser.parseString(mobiledoc).asJsonObject
|
|
||||||
val cards = json.getAsJsonArray("cards") ?: return emptyList()
|
|
||||||
cards.mapNotNull { card ->
|
|
||||||
val cardArray = card.asJsonArray
|
|
||||||
if (cardArray.size() >= 2 && cardArray[0].asString == "image") {
|
|
||||||
val cardData = cardArray[1].asJsonObject
|
|
||||||
cardData.get("src")?.asString
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ private fun PageEditorScreen(
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
onSave(title, content, slug.takeIf { it.isNotBlank() }, selectedStatus)
|
onSave(title, content, slug.takeIf { it.isNotBlank() }, selectedStatus)
|
||||||
} else {
|
} else {
|
||||||
val mobiledoc = MobiledocBuilder.build(content, null, null, null)
|
val mobiledoc = MobiledocBuilder.build(content)
|
||||||
val updatedPage = GhostPage(
|
val updatedPage = GhostPage(
|
||||||
title = title,
|
title = title,
|
||||||
mobiledoc = mobiledoc,
|
mobiledoc = mobiledoc,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class PagesViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
fun savePage(title: String, content: String, slug: String?, status: String) {
|
fun savePage(title: String, content: String, slug: String?, status: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
val mobiledoc = MobiledocBuilder.build(content, null, null, null)
|
val mobiledoc = MobiledocBuilder.build(content)
|
||||||
val page = GhostPage(
|
val page = GhostPage(
|
||||||
title = title,
|
title = title,
|
||||||
mobiledoc = mobiledoc,
|
mobiledoc = mobiledoc,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.swoosh.microblog.data.AccountManager
|
import com.swoosh.microblog.data.AccountManager
|
||||||
|
import com.swoosh.microblog.data.toDisplayUrl
|
||||||
import com.swoosh.microblog.data.NewsletterPreferences
|
import com.swoosh.microblog.data.NewsletterPreferences
|
||||||
import com.swoosh.microblog.data.SiteMetadataCache
|
import com.swoosh.microblog.data.SiteMetadataCache
|
||||||
import com.swoosh.microblog.data.api.ApiClient
|
import com.swoosh.microblog.data.api.ApiClient
|
||||||
|
|
@ -166,10 +167,7 @@ fun SettingsScreen(
|
||||||
val siteUrl = siteData.url
|
val siteUrl = siteData.url
|
||||||
if (!siteUrl.isNullOrBlank()) {
|
if (!siteUrl.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = siteUrl
|
text = siteUrl.toDisplayUrl(),
|
||||||
.removePrefix("https://")
|
|
||||||
.removePrefix("http://")
|
|
||||||
.removeSuffix("/"),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -330,10 +328,7 @@ fun SettingsScreen(
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = activeAccount.blogUrl
|
text = activeAccount.blogUrl.toDisplayUrl(),
|
||||||
.removePrefix("https://")
|
|
||||||
.removePrefix("http://")
|
|
||||||
.removeSuffix("/"),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.swoosh.microblog.data.repository.MemberRepository
|
||||||
import com.swoosh.microblog.data.repository.MemberStats
|
import com.swoosh.microblog.data.repository.MemberStats
|
||||||
import com.swoosh.microblog.data.repository.PostRepository
|
import com.swoosh.microblog.data.repository.PostRepository
|
||||||
import com.swoosh.microblog.data.repository.TagRepository
|
import com.swoosh.microblog.data.repository.TagRepository
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -34,10 +35,9 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
_uiState.update { it.copy(isLoading = true) }
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get local posts
|
// Launch posts, members, and tags fetches in parallel
|
||||||
|
val postsDeferred = async {
|
||||||
val localPosts = repository.getAllLocalPostsList()
|
val localPosts = repository.getAllLocalPostsList()
|
||||||
|
|
||||||
// Get remote posts
|
|
||||||
val remotePosts = mutableListOf<FeedPost>()
|
val remotePosts = mutableListOf<FeedPost>()
|
||||||
var page = 1
|
var page = 1
|
||||||
var hasMore = true
|
var hasMore = true
|
||||||
|
|
@ -71,18 +71,15 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
hasMore = false
|
hasMore = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Safety limit
|
|
||||||
if (page > 20) break
|
if (page > 20) break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove remote duplicates that exist locally
|
|
||||||
val localGhostIds = localPosts.mapNotNull { it.ghostId }.toSet()
|
val localGhostIds = localPosts.mapNotNull { it.ghostId }.toSet()
|
||||||
val uniqueRemotePosts = remotePosts.filter { it.ghostId !in localGhostIds }
|
val uniqueRemotePosts = remotePosts.filter { it.ghostId !in localGhostIds }
|
||||||
|
Pair(localPosts, uniqueRemotePosts)
|
||||||
|
}
|
||||||
|
|
||||||
val stats = OverallStats.calculate(localPosts, uniqueRemotePosts)
|
val membersDeferred = async {
|
||||||
|
try {
|
||||||
// Fetch member stats (non-fatal if it fails)
|
|
||||||
val memberStats = try {
|
|
||||||
val membersResult = memberRepository.fetchAllMembers()
|
val membersResult = memberRepository.fetchAllMembers()
|
||||||
membersResult.getOrNull()?.let { members ->
|
membersResult.getOrNull()?.let { members ->
|
||||||
memberRepository.getMemberStats(members)
|
memberRepository.getMemberStats(members)
|
||||||
|
|
@ -90,15 +87,23 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch tag stats
|
val tagsDeferred = async {
|
||||||
val tagStats = try {
|
try {
|
||||||
tagRepository.fetchTags().getOrNull()
|
tagRepository.fetchTags().getOrNull()
|
||||||
?.sortedByDescending { it.count?.posts ?: 0 }
|
?.sortedByDescending { it.count?.posts ?: 0 }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (localPosts, uniqueRemotePosts) = postsDeferred.await()
|
||||||
|
val memberStats = membersDeferred.await()
|
||||||
|
val tagStats = tagsDeferred.await()
|
||||||
|
|
||||||
|
val stats = OverallStats.calculate(localPosts, uniqueRemotePosts)
|
||||||
|
|
||||||
// Count posts without any tags
|
// Count posts without any tags
|
||||||
val totalPosts = localPosts.size + uniqueRemotePosts.size
|
val totalPosts = localPosts.size + uniqueRemotePosts.size
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class PostUploadWorker(
|
||||||
workerParams: WorkerParameters
|
workerParams: WorkerParameters
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val repository = PostRepository(applicationContext)
|
val repository = PostRepository(applicationContext)
|
||||||
val queuedPosts = repository.getQueuedPosts()
|
val queuedPosts = repository.getQueuedPosts()
|
||||||
|
|
@ -118,7 +120,7 @@ class PostUploadWorker(
|
||||||
|
|
||||||
// Parse tags from JSON stored in LocalPost
|
// Parse tags from JSON stored in LocalPost
|
||||||
val tagNames: List<String> = try {
|
val tagNames: List<String> = try {
|
||||||
Gson().fromJson(post.tags, object : TypeToken<List<String>>() {}.type) ?: emptyList()
|
gson.fromJson(post.tags, object : TypeToken<List<String>>() {}.type) ?: emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with separate params and no link produces same as null preview`() {
|
fun `build with separate params and no link produces same as null preview`() {
|
||||||
val resultA = MobiledocBuilder.build("Hello", null as LinkPreview?)
|
val resultA = MobiledocBuilder.build("Hello", null as LinkPreview?)
|
||||||
val resultB = MobiledocBuilder.build("Hello", null, null, null)
|
val resultB = MobiledocBuilder.build("Hello")
|
||||||
assertEquals(resultA, resultB)
|
assertEquals(resultA, resultB)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,9 +207,9 @@ class MobiledocBuilderTest {
|
||||||
fun `build with separate params includes link data`() {
|
fun `build with separate params includes link data`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text",
|
"Text",
|
||||||
"https://test.com",
|
linkUrl = "https://test.com",
|
||||||
"Test Title",
|
linkTitle = "Test Title",
|
||||||
"Test Desc"
|
linkDescription = "Test Desc"
|
||||||
)
|
)
|
||||||
assertTrue(result.contains("https://test.com"))
|
assertTrue(result.contains("https://test.com"))
|
||||||
assertTrue(result.contains("Test Title"))
|
assertTrue(result.contains("Test Title"))
|
||||||
|
|
@ -219,7 +219,7 @@ class MobiledocBuilderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build with separate params handles null title and description`() {
|
fun `build with separate params handles null title and description`() {
|
||||||
val result = MobiledocBuilder.build("Text", "https://test.com", null, null)
|
val result = MobiledocBuilder.build("Text", linkUrl = "https://test.com")
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
assertTrue(result.contains("bookmark"))
|
assertTrue(result.contains("bookmark"))
|
||||||
|
|
@ -257,8 +257,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card produces valid JSON`() {
|
fun `build with image card produces valid JSON`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Post text", null, null, null,
|
"Post text",
|
||||||
"https://example.com/photo.jpg", "A sunset"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "A sunset"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -267,8 +268,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card includes image type`() {
|
fun `build with image card includes image type`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt text"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "Alt text"
|
||||||
)
|
)
|
||||||
assertTrue("Should contain image card type", result.contains("\"image\""))
|
assertTrue("Should contain image card type", result.contains("\"image\""))
|
||||||
}
|
}
|
||||||
|
|
@ -276,8 +278,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card includes src`() {
|
fun `build with image card includes src`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt text"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "Alt text"
|
||||||
)
|
)
|
||||||
assertTrue("Should contain image src", result.contains("https://example.com/photo.jpg"))
|
assertTrue("Should contain image src", result.contains("https://example.com/photo.jpg"))
|
||||||
}
|
}
|
||||||
|
|
@ -285,8 +288,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card includes alt text`() {
|
fun `build with image card includes alt text`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "A beautiful sunset"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "A beautiful sunset"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
@ -300,8 +304,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card and null alt uses empty string`() {
|
fun `build with image card and null alt uses empty string`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", null
|
imageUrls = listOf("https://example.com/photo.jpg")
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
@ -313,8 +317,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card includes caption field`() {
|
fun `build with image card includes caption field`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "Alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val card = json.getAsJsonArray("cards").get(0).asJsonArray
|
val card = json.getAsJsonArray("cards").get(0).asJsonArray
|
||||||
|
|
@ -325,8 +330,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card has card section`() {
|
fun `build with image card has card section`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "Alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -336,8 +342,12 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image and link has both cards`() {
|
fun `build with image and link has both cards`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", "https://link.com", "Link Title", "Link Desc",
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Image alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
linkUrl = "https://link.com",
|
||||||
|
linkTitle = "Link Title",
|
||||||
|
linkDescription = "Link Desc",
|
||||||
|
imageAlt = "Image alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
@ -350,8 +360,12 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image and link has three sections`() {
|
fun `build with image and link has three sections`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", "https://link.com", "Title", "Desc",
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
linkUrl = "https://link.com",
|
||||||
|
linkTitle = "Title",
|
||||||
|
linkDescription = "Desc",
|
||||||
|
imageAlt = "Alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -361,8 +375,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card escapes alt text`() {
|
fun `build with image card escapes alt text`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "He said \"hello\""
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "He said \"hello\""
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull("Should produce valid JSON with escaped alt text", json)
|
assertNotNull("Should produce valid JSON with escaped alt text", json)
|
||||||
|
|
@ -370,10 +385,7 @@ class MobiledocBuilderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build without image produces no image card`() {
|
fun `build without image produces no image card`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build("Text")
|
||||||
"Text", null, null, null,
|
|
||||||
null, null
|
|
||||||
)
|
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertTrue("Should have no cards", json.getAsJsonArray("cards").isEmpty)
|
assertTrue("Should have no cards", json.getAsJsonArray("cards").isEmpty)
|
||||||
}
|
}
|
||||||
|
|
@ -381,8 +393,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image card section references correct card index`() {
|
fun `build with image card section references correct card index`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", null, null, null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
imageAlt = "Alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -394,8 +407,11 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with image and link card sections reference correct indices`() {
|
fun `build with image and link card sections reference correct indices`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", "https://link.com", "Title", null,
|
"Text",
|
||||||
"https://example.com/photo.jpg", "Alt"
|
imageUrls = listOf("https://example.com/photo.jpg"),
|
||||||
|
linkUrl = "https://link.com",
|
||||||
|
linkTitle = "Title",
|
||||||
|
imageAlt = "Alt"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -414,7 +430,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with single image produces valid JSON`() {
|
fun `build with single image produces valid JSON`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Hello", listOf("https://example.com/img.jpg"), null, null, null
|
"Hello", listOf("https://example.com/img.jpg")
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -423,7 +439,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with single image has one image card`() {
|
fun `build with single image has one image card`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Hello", listOf("https://example.com/img.jpg"), null, null, null
|
"Hello", listOf("https://example.com/img.jpg")
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertEquals(1, json.getAsJsonArray("cards").size())
|
assertEquals(1, json.getAsJsonArray("cards").size())
|
||||||
|
|
@ -436,7 +452,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with single image has two sections`() {
|
fun `build with single image has two sections`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Hello", listOf("https://example.com/img.jpg"), null, null, null
|
"Hello", listOf("https://example.com/img.jpg")
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertEquals(2, json.getAsJsonArray("sections").size())
|
assertEquals(2, json.getAsJsonArray("sections").size())
|
||||||
|
|
@ -449,7 +465,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img2.jpg",
|
"https://example.com/img2.jpg",
|
||||||
"https://example.com/img3.jpg"
|
"https://example.com/img3.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +477,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img2.jpg",
|
"https://example.com/img2.jpg",
|
||||||
"https://example.com/img3.jpg"
|
"https://example.com/img3.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertEquals(3, json.getAsJsonArray("cards").size())
|
assertEquals(3, json.getAsJsonArray("cards").size())
|
||||||
}
|
}
|
||||||
|
|
@ -472,7 +488,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img1.jpg",
|
"https://example.com/img1.jpg",
|
||||||
"https://example.com/img2.jpg"
|
"https://example.com/img2.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
// 1 text section + 2 card sections
|
// 1 text section + 2 card sections
|
||||||
assertEquals(3, json.getAsJsonArray("sections").size())
|
assertEquals(3, json.getAsJsonArray("sections").size())
|
||||||
|
|
@ -484,7 +500,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img1.jpg",
|
"https://example.com/img1.jpg",
|
||||||
"https://example.com/img2.jpg"
|
"https://example.com/img2.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
for (i in 0 until cards.size()) {
|
for (i in 0 until cards.size()) {
|
||||||
|
|
@ -500,7 +516,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img2.jpg",
|
"https://example.com/img2.jpg",
|
||||||
"https://example.com/img3.jpg"
|
"https://example.com/img3.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
assertEquals("https://example.com/img1.jpg", cards.get(0).asJsonArray.get(1).asJsonObject.get("src").asString)
|
assertEquals("https://example.com/img1.jpg", cards.get(0).asJsonArray.get(1).asJsonObject.get("src").asString)
|
||||||
|
|
@ -512,7 +528,7 @@ class MobiledocBuilderTest {
|
||||||
fun `build with images and link has both image and bookmark cards`() {
|
fun `build with images and link has both image and bookmark cards`() {
|
||||||
val images = listOf("https://example.com/img1.jpg")
|
val images = listOf("https://example.com/img1.jpg")
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Hello", images, "https://example.com", "Title", "Desc"
|
"Hello", images, linkUrl = "https://example.com", linkTitle = "Title", linkDescription = "Desc"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
@ -528,7 +544,7 @@ class MobiledocBuilderTest {
|
||||||
fun `build with images and link has correct number of sections`() {
|
fun `build with images and link has correct number of sections`() {
|
||||||
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Hello", images, "https://example.com", "Title", "Desc"
|
"Hello", images, linkUrl = "https://example.com", linkTitle = "Title", linkDescription = "Desc"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
// 1 text section + 2 image card sections + 1 bookmark card section
|
// 1 text section + 2 image card sections + 1 bookmark card section
|
||||||
|
|
@ -538,7 +554,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with images card sections reference correct card indices`() {
|
fun `build with images card sections reference correct card indices`() {
|
||||||
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
||||||
|
|
@ -556,7 +572,7 @@ class MobiledocBuilderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build with empty image list produces no image cards`() {
|
fun `build with empty image list produces no image cards`() {
|
||||||
val result = MobiledocBuilder.build("Hello", emptyList(), null, null, null)
|
val result = MobiledocBuilder.build("Hello", emptyList())
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertTrue(json.getAsJsonArray("cards").isEmpty)
|
assertTrue(json.getAsJsonArray("cards").isEmpty)
|
||||||
assertEquals(1, json.getAsJsonArray("sections").size())
|
assertEquals(1, json.getAsJsonArray("sections").size())
|
||||||
|
|
@ -565,14 +581,14 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with empty image list matches no-image build`() {
|
fun `build with empty image list matches no-image build`() {
|
||||||
val resultA = MobiledocBuilder.build("Hello", null as LinkPreview?)
|
val resultA = MobiledocBuilder.build("Hello", null as LinkPreview?)
|
||||||
val resultB = MobiledocBuilder.build("Hello", emptyList(), null, null, null)
|
val resultB = MobiledocBuilder.build("Hello", emptyList())
|
||||||
assertEquals(resultA, resultB)
|
assertEquals(resultA, resultB)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build with image URL containing special chars produces valid JSON`() {
|
fun `build with image URL containing special chars produces valid JSON`() {
|
||||||
val images = listOf("https://example.com/img?id=1&name=\"test\"")
|
val images = listOf("https://example.com/img?id=1&name=\"test\"")
|
||||||
val result = MobiledocBuilder.build("Hello", images, null, null, null)
|
val result = MobiledocBuilder.build("Hello", images)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
}
|
}
|
||||||
|
|
@ -585,7 +601,7 @@ class MobiledocBuilderTest {
|
||||||
"https://example.com/img1.jpg",
|
"https://example.com/img1.jpg",
|
||||||
"https://example.com/img2.jpg"
|
"https://example.com/img2.jpg"
|
||||||
)
|
)
|
||||||
val result = MobiledocBuilder.build("Text", images, null, null, null, "First image alt")
|
val result = MobiledocBuilder.build("Text", images, imageAlt = "First image alt")
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
||||||
|
|
@ -603,8 +619,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with video only produces valid JSON with video card`() {
|
fun `build with video only produces valid JSON with video card`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Check this video", emptyList(), null, null, null, null,
|
"Check this video",
|
||||||
"https://example.com/video.mp4", null
|
videoUrl = "https://example.com/video.mp4"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -621,8 +637,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with video has correct sections`() {
|
fun `build with video has correct sections`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
"https://example.com/video.mp4", null
|
videoUrl = "https://example.com/video.mp4"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -638,8 +654,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with audio only produces valid JSON with audio card`() {
|
fun `build with audio only produces valid JSON with audio card`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Listen to this", emptyList(), null, null, null, null,
|
"Listen to this",
|
||||||
null, "https://example.com/audio.mp3"
|
audioUrl = "https://example.com/audio.mp3"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -655,8 +671,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with audio has correct sections`() {
|
fun `build with audio has correct sections`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
null, "https://example.com/audio.mp3"
|
audioUrl = "https://example.com/audio.mp3"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val sections = json.getAsJsonArray("sections")
|
val sections = json.getAsJsonArray("sections")
|
||||||
|
|
@ -670,10 +686,12 @@ class MobiledocBuilderTest {
|
||||||
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Full post", images,
|
"Full post", images,
|
||||||
"https://link.com", "Link Title", "Link Desc",
|
linkUrl = "https://link.com",
|
||||||
"Alt text",
|
linkTitle = "Link Title",
|
||||||
"https://example.com/video.mp4",
|
linkDescription = "Link Desc",
|
||||||
"https://example.com/audio.mp3"
|
imageAlt = "Alt text",
|
||||||
|
videoUrl = "https://example.com/video.mp4",
|
||||||
|
audioUrl = "https://example.com/audio.mp3"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -704,9 +722,9 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with video and audio but no images produces correct cards`() {
|
fun `build with video and audio but no images produces correct cards`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Media post", emptyList(), null, null, null, null,
|
"Media post",
|
||||||
"https://example.com/video.mp4",
|
videoUrl = "https://example.com/video.mp4",
|
||||||
"https://example.com/audio.mp3"
|
audioUrl = "https://example.com/audio.mp3"
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
val cards = json.getAsJsonArray("cards")
|
val cards = json.getAsJsonArray("cards")
|
||||||
|
|
@ -718,8 +736,8 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with video URL containing special chars produces valid JSON`() {
|
fun `build with video URL containing special chars produces valid JSON`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
"https://example.com/video?id=1&name=\"test\"", null
|
videoUrl = "https://example.com/video?id=1&name=\"test\""
|
||||||
)
|
)
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertNotNull(json)
|
assertNotNull(json)
|
||||||
|
|
@ -730,7 +748,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with file card produces valid JSON`() {
|
fun `build with file card produces valid JSON`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Post text", emptyList(), null, null, null, null,
|
"Post text",
|
||||||
fileUrl = "https://example.com/files/report.pdf",
|
fileUrl = "https://example.com/files/report.pdf",
|
||||||
fileName = "report.pdf",
|
fileName = "report.pdf",
|
||||||
fileSize = 102400
|
fileSize = 102400
|
||||||
|
|
@ -742,7 +760,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with file card includes file type`() {
|
fun `build with file card includes file type`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
fileUrl = "https://example.com/files/doc.pdf",
|
fileUrl = "https://example.com/files/doc.pdf",
|
||||||
fileName = "doc.pdf",
|
fileName = "doc.pdf",
|
||||||
fileSize = 5000
|
fileSize = 5000
|
||||||
|
|
@ -757,7 +775,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with file card includes src fileName and fileSize`() {
|
fun `build with file card includes src fileName and fileSize`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
fileUrl = "https://example.com/files/report.pdf",
|
fileUrl = "https://example.com/files/report.pdf",
|
||||||
fileName = "report.pdf",
|
fileName = "report.pdf",
|
||||||
fileSize = 204800
|
fileSize = 204800
|
||||||
|
|
@ -773,7 +791,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with file card has correct section count`() {
|
fun `build with file card has correct section count`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
fileUrl = "https://example.com/file.pdf",
|
fileUrl = "https://example.com/file.pdf",
|
||||||
fileName = "file.pdf",
|
fileName = "file.pdf",
|
||||||
fileSize = 1000
|
fileSize = 1000
|
||||||
|
|
@ -787,7 +805,11 @@ class MobiledocBuilderTest {
|
||||||
fun `build with file card comes after image and bookmark cards`() {
|
fun `build with file card comes after image and bookmark cards`() {
|
||||||
val images = listOf("https://example.com/img.jpg")
|
val images = listOf("https://example.com/img.jpg")
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", images, "https://link.com", "Link Title", "Desc", "Alt",
|
"Text", images,
|
||||||
|
linkUrl = "https://link.com",
|
||||||
|
linkTitle = "Link Title",
|
||||||
|
linkDescription = "Desc",
|
||||||
|
imageAlt = "Alt",
|
||||||
fileUrl = "https://example.com/file.pdf",
|
fileUrl = "https://example.com/file.pdf",
|
||||||
fileName = "file.pdf",
|
fileName = "file.pdf",
|
||||||
fileSize = 1024
|
fileSize = 1024
|
||||||
|
|
@ -805,7 +827,10 @@ class MobiledocBuilderTest {
|
||||||
fun `build with file card and all attachments has correct section count`() {
|
fun `build with file card and all attachments has correct section count`() {
|
||||||
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
val images = listOf("https://example.com/img1.jpg", "https://example.com/img2.jpg")
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", images, "https://link.com", "Title", "Desc", null,
|
"Text", images,
|
||||||
|
linkUrl = "https://link.com",
|
||||||
|
linkTitle = "Title",
|
||||||
|
linkDescription = "Desc",
|
||||||
fileUrl = "https://example.com/file.pdf",
|
fileUrl = "https://example.com/file.pdf",
|
||||||
fileName = "file.pdf",
|
fileName = "file.pdf",
|
||||||
fileSize = 500
|
fileSize = 500
|
||||||
|
|
@ -817,10 +842,7 @@ class MobiledocBuilderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build without file produces no file card`() {
|
fun `build without file produces no file card`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build("Text")
|
||||||
"Text", emptyList(), null, null, null, null,
|
|
||||||
fileUrl = null, fileName = null, fileSize = 0
|
|
||||||
)
|
|
||||||
val json = JsonParser.parseString(result).asJsonObject
|
val json = JsonParser.parseString(result).asJsonObject
|
||||||
assertTrue(json.getAsJsonArray("cards").isEmpty)
|
assertTrue(json.getAsJsonArray("cards").isEmpty)
|
||||||
}
|
}
|
||||||
|
|
@ -828,7 +850,7 @@ class MobiledocBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `build with file card escapes fileName`() {
|
fun `build with file card escapes fileName`() {
|
||||||
val result = MobiledocBuilder.build(
|
val result = MobiledocBuilder.build(
|
||||||
"Text", emptyList(), null, null, null, null,
|
"Text",
|
||||||
fileUrl = "https://example.com/file.pdf",
|
fileUrl = "https://example.com/file.pdf",
|
||||||
fileName = "my \"special\" file.pdf",
|
fileName = "my \"special\" file.pdf",
|
||||||
fileSize = 100
|
fileSize = 100
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue