Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,19 +183,28 @@ fun endItemShape(): RoundedCornerShape = RoundedCornerShape(

fun detachedItemShape(): RoundedCornerShape = RoundedCornerShape(EndCornerRadius.dp)

/**
* Parses markdown formatted text into an [androidx.compose.ui.text.AnnotatedString]
* supporting bold, italic, inline code, and interactive links with URL annotations.
*
* @return An [androidx.compose.ui.text.AnnotatedString] with formatted styles and link annotations.
*/
@Composable
fun String.parseMarkdown(): androidx.compose.ui.text.AnnotatedString {
val cleanText = this
.replace(Regex("^(?:[-*+•]|\\d+\\.)\\s+"), "")
.replace(Regex("\\[\\[([^\\]]+)\\]\\(([^)]+)\\)\\](?:\\([^)]+\\))?"), "[$1]($2)")
val builder = androidx.compose.ui.text.AnnotatedString.Builder()
var currentIndex = 0
val primaryColor = MaterialTheme.colorScheme.primary
val codeBgColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)

val pattern = Regex("(\\*\\*(.*?)\\*\\*)|(\\*([^*]+)\\*)|(`([^`]+)`)|(\\[([^\\]]+)\\]\\(([^)]+)\\))|((?:https?://|www\\.)[\\w-]+(?:\\.[\\w-]+)+(?:[/?][\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]*)?)")

val matches = pattern.findAll(this)
val matches = pattern.findAll(cleanText)
for (match in matches) {
if (match.range.first > currentIndex) {
builder.append(this.substring(currentIndex, match.range.first))
builder.append(cleanText.substring(currentIndex, match.range.first))
}

when {
Expand Down Expand Up @@ -245,8 +254,8 @@ fun String.parseMarkdown(): androidx.compose.ui.text.AnnotatedString {
currentIndex = match.range.last + 1
}

if (currentIndex < this.length) {
builder.append(this.substring(currentIndex))
if (currentIndex < cleanText.length) {
builder.append(cleanText.substring(currentIndex))
}

return builder.toAnnotatedString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,22 @@ import echo.music.iad1tya.echomusic.updater.getBetaUpdatesSetting
import echo.music.iad1tya.echomusic.updater.saveBetaUpdatesSetting
import echo.music.iad1tya.echomusic.updater.autoClearOldApks
import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.text.font.FontWeight
import echo.music.iad1tya.ui.utils.parseMarkdownToSections
import echo.music.iad1tya.ui.utils.parseSimpleMarkdown
import echo.music.iad1tya.BuildConfig
import org.json.JSONObject

/**
* Settings screen for managing app updates, checking for new releases,
* and displaying the latest release notes ("What's New").
*
* @param navController Navigation controller for screen transitions.
* @param scrollBehavior Top app bar scroll behavior.
* @param highlightKey Optional key to highlight a specific settings item.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdateSettings(
Expand Down Expand Up @@ -240,12 +253,70 @@ fun UpdateSettings(
),
elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Text(
text = echo.music.iad1tya.ui.utils.parseSimpleMarkdown(notes),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(20.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
val (effectiveDescription, effectiveSections) = remember(notes) {
parseMarkdownToSections(notes)
}

if (!effectiveDescription.isNullOrBlank()) {
Text(
text = parseSimpleMarkdown(effectiveDescription, MaterialTheme.colorScheme.primary),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp)
)
}

if (effectiveSections.isNotEmpty()) {
effectiveSections.forEachIndexed { sectionIndex, section ->
if (section.title.isNotBlank()) {
Text(
text = parseSimpleMarkdown(section.title, MaterialTheme.colorScheme.primary),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(
top = if (sectionIndex == 0 && effectiveDescription.isNullOrBlank()) 0.dp else 10.dp,
bottom = 4.dp
)
)
}
section.items.forEach { item ->
if (item.isNotBlank()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "•",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)
Text(
text = parseSimpleMarkdown(item.trim(), MaterialTheme.colorScheme.primary),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
}
}
}
}
} else if (effectiveDescription.isNullOrBlank()) {
Text(
text = parseSimpleMarkdown(notes, MaterialTheme.colorScheme.primary),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/kotlin/com/music/echo/ui/utils/MarkdownParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ fun parseSimpleMarkdown(
text: String,
primaryColor: Color = Color.Unspecified
): AnnotatedString {
// Strip leading bullet marker if the item itself starts with one
val cleanText = text.replace(Regex("^(?:[-*+•]|\\d+\\.)\\s+"), "")
// Strip leading bullet marker if the item itself starts with one and normalize nested markdown links (e.g. GitHub release [[user](url)](url))
val cleanText = text
.replace(Regex("^(?:[-*+•]|\\d+\\.)\\s+"), "")
.replace(Regex("\\[\\[([^\\]]+)\\]\\(([^)]+)\\)\\](?:\\([^)]+\\))?"), "[$1]($2)")
val pattern = Regex(
"(\\*\\*(.*?)\\*\\*)|" + // 1, 2: **bold**
"(\\*([^*]+)\\*)|" + // 3, 4: *italic*
Expand Down
Loading