Add markdown support for manga descriptions (mihonapp/mihon#1948)

(cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1)
This commit is contained in:
Secozzi 2025-04-01 08:52:15 +02:00 committed by Cuong-Tran
parent cd25a7d443
commit 7f196f128b
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
8 changed files with 167 additions and 61 deletions

View file

@ -15,6 +15,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909)) - Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909))
- Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428)) - Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428))
- Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945)) - Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945))
- Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948))
### Fixes ### Fixes
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885)) - Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))

View file

@ -280,7 +280,6 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.richeditor.compose) implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
@ -289,6 +288,7 @@ dependencies {
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable) implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// KMK --> // KMK -->
implementation(libs.palette.ktx) implementation(libs.palette.ktx)

View file

@ -79,6 +79,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotatorConfig
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -106,8 +108,6 @@ import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
import tachiyomi.domain.manga.model.MangaCover as DomainMangaCover import tachiyomi.domain.manga.model.MangaCover as DomainMangaCover
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable @Composable
fun MangaInfoBox( fun MangaInfoBox(
isTabletUi: Boolean, isTabletUi: Boolean,
@ -354,14 +354,9 @@ fun ExpandableMangaDescription(
} }
val desc = val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary( MangaSummary(
expandedDescription = desc, description = desc,
shrunkDescription = trimmedDescription,
expanded = expanded, expanded = expanded,
notes = notes, notes = notes,
onEditNotesClicked = onEditNotes, onEditNotesClicked = onEditNotes,
@ -828,10 +823,15 @@ private fun ColumnScope.MangaContentInfo(
} }
} }
private val descriptionAnnotator = markdownAnnotator(
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
@Composable @Composable
private fun MangaSummary( private fun MangaSummary(
expandedDescription: String, description: String,
shrunkDescription: String,
notes: String, notes: String,
expanded: Boolean, expanded: Boolean,
onEditNotesClicked: () -> Unit, onEditNotesClicked: () -> Unit,
@ -859,9 +859,10 @@ private fun MangaSummary(
expanded = true, expanded = true,
onEditNotes = onEditNotesClicked, onEditNotes = onEditNotesClicked,
) )
Text( MarkdownRender(
text = expandedDescription, content = description,
style = MaterialTheme.typography.bodyMedium, annotator = descriptionAnnotator,
modifier = Modifier.secondaryItemAlpha(),
) )
} }
}, },
@ -873,11 +874,9 @@ private fun MangaSummary(
onEditNotes = onEditNotesClicked, onEditNotes = onEditNotesClicked,
) )
SelectionContainer { SelectionContainer {
Text( MarkdownRender(
text = if (expanded) expandedDescription else shrunkDescription, content = description,
maxLines = Int.MAX_VALUE, annotator = descriptionAnnotator,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(), 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 package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -13,12 +14,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown import eu.kanade.presentation.manga.components.MarkdownRender
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
@ -44,17 +41,12 @@ fun ComingUpdatesScreen(
rejectText = stringResource(MR.strings.action_not_now), rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate, onRejectClick = onRejectUpdate,
) { ) {
RichText( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large), .padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) { ) {
Markdown(content = changelogInfo) MarkdownRender(content = changelogInfo)
TextButton( TextButton(
onClick = onOpenInBrowser, onClick = onOpenInBrowser,

View file

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

View file

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

View file

@ -3,7 +3,6 @@ aboutlib_version = "11.6.3"
leakcanary = "2.14" leakcanary = "2.14"
moko = "0.24.5" moko = "0.24.5"
okhttp_version = "5.0.0-alpha.14" okhttp_version = "5.0.0-alpha.14"
richtext = "0.20.0"
shizuku_version = "13.1.0" shizuku_version = "13.1.0"
sqldelight = "2.0.2" sqldelight = "2.0.2"
sqlite = "2.4.0" sqlite = "2.4.0"
@ -11,6 +10,7 @@ voyager = "1.0.1"
spotless = "7.0.2" spotless = "7.0.2"
ktlint-core = "1.5.0" ktlint-core = "1.5.0"
firebase-bom = "33.12.0" firebase-bom = "33.12.0"
markdown = "0.33.0-b05"
[libraries] [libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.1.5" desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
@ -52,9 +52,6 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540"
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10" richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10"
material = "com.google.android.material:material:1.12.0" material = "com.google.android.material:material:1.12.0"
@ -107,6 +104,9 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers
spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" } spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
ktlint-core = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint-core" } ktlint-core = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint-core" }
markdown-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdown" }
markdown-coil = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", version.ref = "markdown" }
[plugins] [plugins]
google-services = { id = "com.google.gms.google-services", version = "4.4.2" } google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" }
@ -122,5 +122,5 @@ coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
richtext = ["richtext-commonmark", "richtext-m3"]
test = ["junit", "kotest-assertions", "mockk"] test = ["junit", "kotest-assertions", "mockk"]
markdown = ["markdown-m3", "markdown-coil"]