Add markdown support for manga descriptions (#1948)

This commit is contained in:
Secozzi
2025-04-01 08:52:15 +02:00
committed by GitHub
parent c8ffabc84a
commit 4e68339783
6 changed files with 159 additions and 37 deletions

View File

@ -262,7 +262,6 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
@ -271,6 +270,7 @@ dependencies {
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging
implementation(libs.logcat)

View File

@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotatorConfig
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
@ -94,8 +96,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun MangaInfoBox(
isTabletUi: Boolean,
@ -248,14 +248,9 @@ fun ExpandableMangaDescription(
}
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
description = desc,
expanded = expanded,
notes = notes,
onEditNotesClicked = onEditNotes,
@ -559,10 +554,15 @@ private fun ColumnScope.MangaContentInfo(
}
}
private val descriptionAnnotator = markdownAnnotator(
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
@Composable
private fun MangaSummary(
expandedDescription: String,
shrunkDescription: String,
description: String,
notes: String,
expanded: Boolean,
onEditNotesClicked: () -> Unit,
@ -590,9 +590,10 @@ private fun MangaSummary(
expanded = true,
onEditNotes = onEditNotesClicked,
)
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
MarkdownRender(
content = description,
annotator = descriptionAnnotator,
modifier = Modifier.secondaryItemAlpha(),
)
}
},
@ -604,11 +605,9 @@ private fun MangaSummary(
onEditNotes = onEditNotesClicked,
)
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
MarkdownRender(
content = description,
annotator = descriptionAnnotator,
modifier = Modifier.secondaryItemAlpha(),
)
}

View File

@ -0,0 +1,130 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
import com.mikepenz.markdown.compose.elements.MarkdownDivider
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
import com.mikepenz.markdown.compose.elements.MarkdownTable
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
import com.mikepenz.markdown.compose.elements.listDepth
import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography
import com.mikepenz.markdown.model.MarkdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownPadding
import tachiyomi.presentation.core.components.material.padding
@Composable
fun MarkdownRender(
content: String,
annotator: MarkdownAnnotator = markdownAnnotator(),
modifier: Modifier = Modifier,
) {
Markdown(
content = content,
annotator = annotator,
typography = mihonMarkdownTypography(),
padding = mihonMarkdownPadding(),
components = mihonMarkdownComponents(),
imageTransformer = Coil3ImageTransformerImpl,
modifier = modifier,
)
}
@Composable
private fun mihonMarkdownPadding() = markdownPadding(
list = 0.dp,
listItemTop = 2.dp,
listItemBottom = 2.dp,
)
@Composable
private fun mihonMarkdownTypography() = markdownTypography(
h1 = MaterialTheme.typography.headlineMedium,
h2 = MaterialTheme.typography.headlineSmall,
h3 = MaterialTheme.typography.titleLarge,
h4 = MaterialTheme.typography.titleMedium,
h5 = MaterialTheme.typography.titleSmall,
h6 = MaterialTheme.typography.bodyLarge,
paragraph = MaterialTheme.typography.bodyMedium,
text = MaterialTheme.typography.bodyMedium,
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
),
)
@Composable
private fun mihonMarkdownComponents() = markdownComponents(
horizontalRule = {
MarkdownDivider(
modifier = Modifier
.padding(vertical = MaterialTheme.padding.extraSmall)
.fillMaxWidth(),
)
},
orderedList = { ol ->
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownOrderedList(
content = ol.content,
node = ol.node,
style = ol.typography.ordered,
depth = ol.listDepth,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
},
unorderedList = { ul ->
val markers = listOf("", "", "", "")
CompositionLocalProvider(
LocalBulletListHandler provides { _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
) {
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownBulletList(ul.content, ul.node, style = ul.typography.bullet)
}
}
},
table = { t ->
MarkdownTable(
content = t.content,
node = t.node,
style = t.typography.text,
headerBlock = { content, header, tableWidth, style ->
MarkdownTableHeader(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
rowBlock = { content, header, tableWidth, style ->
MarkdownTableRow(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
)
},
)

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -13,12 +14,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.manga.components.MarkdownRender
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
@ -42,17 +39,12 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate,
) {
RichText(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = changelogInfo)
MarkdownRender(content = changelogInfo)
TextButton(
onClick = onOpenInBrowser,