From ad53c0de83d8cb840d466084b23ce5be6113fc11 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:52:15 +0200 Subject: [PATCH] Add markdown support for manga descriptions (#1948) (cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1) # Conflicts: # CHANGELOG.md # gradle/libs.versions.toml --- app/build.gradle.kts | 4 +- .../manga/components/MangaInfoHeader.kt | 37 +++-- .../manga/components/MarkdownRender.kt | 130 ++++++++++++++++++ .../presentation/more/NewUpdateScreen.kt | 16 +-- core/common/build.gradle.kts | 2 +- domain/build.gradle.kts | 2 +- gradle/libs.versions.toml | 14 +- presentation-widget/build.gradle.kts | 2 +- source-api/build.gradle.kts | 2 +- 9 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b7652861a..b0d4ea46e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -239,7 +239,7 @@ dependencies { implementation(libs.preferencektx) // Dependency injection - implementation(libs.injekt.core) + implementation(libs.injekt) // Image loading implementation(platform(libs.coil.bom)) @@ -257,7 +257,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) @@ -266,6 +265,7 @@ dependencies { implementation(libs.compose.webview) implementation(libs.compose.grid) implementation(libs.reorderable) + implementation(libs.bundles.markdown) // Logging implementation(libs.logcat) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 2683c782f..337b7efcf 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -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 @@ -95,8 +97,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, @@ -266,14 +266,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, @@ -598,10 +593,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, @@ -629,9 +629,10 @@ private fun MangaSummary( expanded = true, onEditNotes = onEditNotesClicked, ) - Text( - text = expandedDescription, - style = MaterialTheme.typography.bodyMedium, + MarkdownRender( + content = description, + annotator = descriptionAnnotator, + modifier = Modifier.secondaryItemAlpha(), ) } }, @@ -643,11 +644,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(), ) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt new file mode 100644 index 000000000..4d4278b81 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt @@ -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, + ) + }, + ) + }, +) diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index 87dd1ee4d..2fa1dbcdd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -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, diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 9e0458a9a..2db4054ab 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -57,7 +57,7 @@ dependencies { // SY --> implementation(sylibs.xlog) - implementation(libs.injekt.core) + implementation(libs.injekt) implementation(sylibs.exifinterface) // SY <-- } diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index adcb3862b..c6ce62dcb 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { compileOnly(libs.compose.stablemarker) // SY --> - implementation(libs.injekt.core) + implementation(libs.injekt) // SY <-- testImplementation(libs.bundles.test) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07677d350..d96ff72aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,14 +3,14 @@ aboutlib_version = "11.6.3" leakcanary = "2.14" moko = "0.24.5" okhttp_version = "5.0.0-alpha.14" -richtext = "0.20.0" -shizuku_version = "13.1.5" +shizuku_version = "13.1.0" sqldelight = "2.0.2" sqlite = "2.4.0" voyager = "1.0.1" spotless = "7.0.2" ktlint-core = "1.5.0" firebase-bom = "33.11.0" +markdown = "0.33.0-b05" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.1.5" @@ -40,7 +40,7 @@ sqlite-android = "com.github.requery:sqlite-android:3.45.0" preferencektx = "androidx.preference:preference-ktx:1.2.1" -injekt-core = "com.github.null2264:injekt-koin:ee267b2e27" +injekt = "com.github.null2264:injekt-koin:ee267b2e27" coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.1.0" } coil-core = { module = "io.coil-kt.coil3:coil" } @@ -53,9 +53,6 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540" 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" material = "com.google.android.material:material:1.12.0" @@ -104,6 +101,9 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers 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" } +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] google-services = { id = "com.google.gms.google-services", version = "4.4.2" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" } @@ -119,5 +119,5 @@ coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"] shizuku = ["shizuku-api", "shizuku-provider"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] -richtext = ["richtext-commonmark", "richtext-m3"] test = ["junit", "kotest-assertions", "mockk"] +markdown = ["markdown-m3", "markdown-coil"] diff --git a/presentation-widget/build.gradle.kts b/presentation-widget/build.gradle.kts index e9a4f00f9..f22188ea9 100644 --- a/presentation-widget/build.gradle.kts +++ b/presentation-widget/build.gradle.kts @@ -31,5 +31,5 @@ dependencies { implementation(libs.material) // SY <-- - api(libs.injekt.core) + api(libs.injekt) } diff --git a/source-api/build.gradle.kts b/source-api/build.gradle.kts index 60ab974d7..ef23ca59b 100644 --- a/source-api/build.gradle.kts +++ b/source-api/build.gradle.kts @@ -13,7 +13,7 @@ kotlin { val commonMain by getting { dependencies { api(kotlinx.serialization.json) - api(libs.injekt.core) + api(libs.injekt) api(libs.rxjava) api(libs.jsoup)