diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d38bb55..aa2b11274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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)) +- Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948)) ### Improved - Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946)) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 425b42616..f02e0ef20 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) 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 32b315985..b33db64e8 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 @@ -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(), ) } 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cc1310bb..b9e448b1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ 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.0" sqldelight = "2.0.2" sqlite = "2.4.0" @@ -11,6 +10,7 @@ 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" @@ -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"]